Release notes¶
User-facing release notes for the Viper runtime and the two dsviper bindings.
There is one runtime contract — Viper C++ on the 1.2 line (MAJOR.MINOR) —
delivered through two installable packages, each with its own independent
PATCH stream:
dsviper for Python — the wheel on PyPI (
pip install dsviper)@digitalsubstrate/dsviper — the Node.js binding on npm
See Naming for how these names relate. The Viper C++ runtime is not installed on its own — it ships inside both packages; each binding entry notes the runtime version it carries. Only released versions are listed, and breaking changes are flagged inline.
What LTS-1.2 guarantees¶
The label was applied early. The line was still in late alpha when it went on, and additions were still arriving four months after it. What follows is what the commitment covers — narrower than the name suggests, and the part that has held.
Three things are frozen for the life of the line. The type and value
system — Definitions, Type, Value and the relations between them: a model
that types correctly on one 1.2.x types correctly on every later one. DSM
governance — what a .dsm may declare, and the rules deciding whether a set of
definitions is expressible. And the on-disk format: a database written by any
1.2.x is readable by any other. No migration is needed inside the line, and none
is offered.
Patches are a quality programme, not a feature stream. What a patch carries is a defect closed — a docstring describing what the code does not do, a guard that never fired, a value that could be corrupted. The rule has not been absolute: a third serialization dialect arrived in July, two months past the lock, and that is a feature by any reading. A patch may also break something, and each time that is a severity judgement — leaving a data-corrupting API in place under a deprecation was judged the worse trade every time. Both kinds are listed below and breaks are flagged inline, so neither claim rests on being believed.
The freeze is what opened the ecosystem. Viper was one private repository
holding the runtime and every tool built on it. On a single day in May 2026, seven
repositories were created out of it — kibo, kibo-template-viper, and the
dsviper-* tools and sample applications — each with its own changelog, its own
version, and a surface someone outside can read. The per-artifact changelogs did
not precede that split; they were written during it. What came next is the part
that counts: dsviper-jsonrpc, dsviper-query and dsviper-database-tools were
written after the lock, in pure Python over the published binding and nothing
else — one of them on top of another. A contract you can build new work on is the
only evidence that freezing it was worth doing.
Semantic versioning is operational everywhere except the runtime. Every
satellite versions itself strictly and independently: dsviper-query is at 0.1.0,
dsviper-database-tools at 0.2.4, and neither number tracks 1.2. Viper does not
yet — it is the runtime, it is still stabilising, and the breaking changes listed
below are what that looks like.
Viper C++ runtime¶
The engine shipped inside both bindings; viperVersion() reports this version.
Binding- or packaging-only releases are omitted — except a phantom version (a runtime
number minted with no runtime change, from a lockstep bump), listed to explain the gap.
1.2.27 — 2026-09-27¶
Added
DatabaseToCommitDatabaseConverter::commitId()andCommitDatabaseFlattener::commitId()answer the commit the transfer made. Both refuse a second run, so the answer is never ambiguous.DatabaseTransferErrors, raised when a transfer is asked to run twice.
Fixed
A blob pack writes the same bytes for the same descriptor: each region record carried the three padding bytes of its
BlobLayout, which nothing wrote, so two equal packs could differ, and so could theirBlobId.Html.documentescapes its title: a title carrying markup closed<title>early and could put a script in the head.body()still passes its fragment through raw.Decoding a value refuses a blob holding bytes past it (
StreamErrors::BytesPastTheValue, 3): what remained meant the value read was wrong too, another type or another codec.A locked file answers
SQLiteErrors::Busy(31) wherever the wait runs out:prepareanswered it withFunction(30), so a reader opening a file held exclusively saw “prepare failed”.A blob read or write tells a missing blob (
UnknownBlob, 33) from an offset out of range; not-frozen and frozen carry their own codes, 30 and 32, not the 2 GB one.A merge decree applies only to its own merge:
reconcilerefuses a commit that is not a merge, andreconcile/materializeMergerefuse a resolution analysed against another (ours, theirs), the reversed pair included, before writing anything.A call on a closed
ServiceRemoteorDatabaseRemoteis refused as closed (ServiceErrors::IsClosed,DatabaseRemoteErrors::IsClosed), asCommitDatabaseRemotealready was.DatabaseRemotesent on the released descriptor and reported “Bad file descriptor”.DatabaseRemote::extendDefinitionsrefreshes the definitions the client holds: an attachment it added was refused as unregistered until a reconnection.ServiceRemote::peernameover a unix socket is the path connected to; it was the client’s descriptor number.A value unwrapped from an optional has the optional as its
DocumentNodeparent; it named itself, so nothing walked up from it. Ananydisplaysvalue is <type>.(it showed a stray%1), avarianta space before its type.SharedMemory::fd()answers the descriptor the region is mapped from (-1 on Windows). A descriptor of 0 is now closed, and a failedcreatereleases its descriptor and name. On Windows,createrefuses a name already published, as on POSIX.Semaphore::tryWaitanswers false on a zero count, as documented; it threw on POSIX and answered true on Windows. On Windows,createrefuses a name already published, an opened semaphore can post, and the count is no longer capped at one.A DSM source map cuts the text it names: a documented field’s type span started at its docstring, a
key<>,vec<>ormat<>occurrence at its element name, so a rewrite deleted a docstring or truncated a type.Definitions::createMembershiprecords the membership in its own registry, not on the club object it is handed, which may belong to another.A file cannot take the path
InMemory, the one a database in memory answers:SQLiterefuses to create or open it (SQLiteErrors::ReservedPath), andisCompatibleanswers false.inMemory()answered true for such a file.CommitDatabaseServer::stepanswers false once the server stopped or was cancelled; it kept answering true, so a loop on it never ended.Socket::acceptNonBlockrefuses a closed socket, aswaitReadabledoes, instead of selecting on a released descriptor.Deleting a blob stream drops it.
blobStreamDelete, and a close onto a blob already held, matched the wrong key: the stream stayed writable, and one aCommitDatabaseabandoned stayed in the file.DatabaseSQLite::blobStreamDeleterequires a transaction, like every other blob write.freezeBlobanswers false for a blob already frozen, as documented; it answered true.CommitDatabaseSQLite::createBlobsjoins a transaction the caller opened. It opened its own, failed, and its rollback discarded the caller’s earlier writes.An encoder refusal says where the value sits, as
while encoding '.deep[1]'. It said what was wrong and never which value, so a wide document named no culprit. The decoder side already carried its path.A runtime message names a mechanism, not a call. A blob past 2 GB named
readBlob(blobId, size, offset)and a repeated transfer namedcommitId— spellings a Python caller does not have.A failed blob-stream write abandons the stream and keeps its error. The cleanup closed the stream, which refuses an incomplete one, so it threw: the original error was lost, the delete skipped, and the stream stayed usable.
BlobStream::appendrefuses a chunk past the end (BlobStreamErrors::ExceedingBytes) instead of wrapping itssize_tcounter.json_encoderefuses infinity and not-a-number (TypeErrors::InvalidJsonNumber) instead of writingnull, which left the document complete and failed on whoever read it next. The stream codecs and XML carry them, unchanged.bson_encoderefuses what BSON cannot hold: those same numbers, and an integer above its signed 64-bit range (TypeErrors::InvalidBsonInteger), which escaped as the writing library’s own exception rather than a Viper error.DatabaseSQLite::isCompatibleanswers instead of throwing for a file that is not SQLite3. It opened whatever it was handed;CommitDatabaseSQLite::isCompatiblealready guarded withSQLite::isSQLite3.CommitStore::extendDefinitionsleaves the store able to write. It kept the state built atuse()on the old definitions, so every later dispatch failed silently through the notifier, writing no commit.to_dsm()names its second block// Mapping for Core::User.profile, where it said only// Definitionwith a trailing space.The default HTML stylesheet defines
details_indent, the class its nesting renderer emits, so nested documents no longer render flat.SQLiteTableBlobErrorsnamesBlobStreamandreadBlobinstead of aBlobIOAPI that exists nowhere, and its component lost a trailing space;DSMErrorssaysDSMTypeReference.Path::patchrefuses a path that addresses no key, naming the shape it is not. An entry with nothing after it answered “invalid index 2”, and one whose index addressed the entry’s value had its key replaced instead.TypeVec::makeandTypeMat::makerefuse a dimension of zero, which holds no data, as the empty enumeration, structure and variant already do. The DSM checker reports it against the model’s own line.
1.2.26 — 2026-09-20¶
Changed
An RPC message holds its payload twice at most while it crosses the wire, not three times.
Removed (breaking)
ValueBlob::make(std::size_t)andBlobView::make(BlobLayout const &, std::size_t)— both made a blob of zeros whose hash andBlobIdwere computed on those zeros, before the caller overwrote them. Use a builder.
Added
BlobArrayBuilderandBlobPackBuilderfill the bytes of a blob, then seal them, withBlobBuilderErrorsfor a builder asked to write afterbuild().
Fixed
A document is written with the type its attachment declares, and a namespace cycle closed through an attachment or a key reference is now seen: the dependency graph missed both.
An RPC peer can no longer make the reader reserve what it never sends, and
CommitDatabaseRemote::uploadSpeed/downloadSpeedreport the direction they measure.A database repository server confines
setDatabaseto its own folder, andXArray::operator!=compiles.
1.2.25 — 2026-08-27¶
Changed
FunctionLambdais renamedFunctionCallableand takes its name at construction: every instance was named"lambda", so aFunctionPool, which indexes by name, kept only the last one.
Removed (breaking)
RPCPacketReturnBlobId, the answer of the id-lesscreateBlobthat 1.2.24 removed.RPCSideClientCall::returnVectorOfUUId, declared and never defined.
Fixed
TypeTupleandTypeVariantanswer the representation of the namespace asked for: they cached the first answer whatever the namespace, and filled that cache unsynchronised from server threads.
1.2.24 — 2026-08-04¶
Changed
Every error domain string names its namespace:
DatabaseErrors::Domainis"DatabaseErrors", and so on.
Removed (breaking)
Five surfaces nothing called, five guards nothing reached, the
UnsetDatabasepacket andRPCPacketReturnOptionalInt64.The id-less
createBlobpacket.
Added
Servers stop when asked:
CommitDatabaseServer,ServiceServerand the repository server runstep(timeoutInSec)under aCancelation, andfinishBefore(sec)answers how many client threads it could not join.RPCConnection::stepFortells a read that timed out from a closed connection, andSocket::waitReadablewaits for a peer without blocking the accept loop.
Fixed
createZeroBlobworks over RPC: the server answered anint64where the client waited for abool, so every remote call failed (RPCProtocolErrors, 10) and blobs could not be streamed to a remote database.A partial
send()no longer truncates a message on POSIX, and the accept loop rebuilds the descriptor setselect()modified.A client thread that throws, or cannot open its database, no longer aborts or hangs the server.
A length header announcing an empty payload is refused.
Windows builds without ATL:
UuidCreatecomes from<rpc.h>.A vec or mat field given a scalar default raises
notALiteralList, notnotALiteralValue; three XML decoder guards that were declared are now applied.dsm_checkandDSMHelper::assemblerefuse a path that does not exist, and the repository server exits non-zero when it cannot start.
1.2.23 — 2026-07-23¶
Fixed
CommitState::getreturns a value its cache does not share: a first read handed out the cached value itself, so mutating it changed what every later read of that key returned.
1.2.22 — 2026-07-22¶
Added
DSMSourceMap: passed toDSMBuilder::parse, it records the source span of every declaration, field, case, namespace, type expression and resolved reference, so a.dsmcan be edited in place.
Fixed
A
CommitMergeResolutioncopies its chosen value: changing that value after building the resolution changed the decree.The ambiguous-reference diagnostic spells “ambiguous”.
1.2.21 — 2026-07-19¶
Changed
A
ValueStringholds UTF-8:ValueString::makerefuses anything else (TypeErrors::InvalidUtf8). A blob holds bytes.Documentation cannot contain
""", which the DSM cannot escape; it is refused at construction and at JSON or XML import (TypeErrors::InvalidDocumentation).
Added
DSM syntax errors speak DSM: the offending token is quoted and what was expected is named (
unexpected \strcut`; expected a definition or `}``). Error recovery is unchanged.
Fixed
The HTML renderer escapes what it prints: a name, string or docstring carrying
<,&or a quote injected markup.The XML value codec round-trips any string: it cut at
U+0000, turned CR into LF and dropped whitespace-only text. XML-forbidden controls are refused (TypeErrors::InvalidXmlText).Strings are escaped wherever they enter DSM,
repror a docstring: a quote or a backslash produced text that did not parse back.A name arriving through JSON or XML import is checked as an identifier (
InvalidName).A DSM error at end of input names its file and a 1-based column.
A variant’s arms are de-duplicated by
runtimeId, not by description.
1.2.20 — 2026-07-12¶
Added
CommitIdCollectorgathers everyCommitIda value references, asBlobIdCollectordoes for blobs;useCommitId(type)says whether a type can hold one.
Fixed
Extending the definitions of a database with a type redefined under a new
runtimeIdis refused before any write (conflictInTypeNameGovernance); it stored a second definition of the name, and the database no longer opened.The construction API refuses what the DSM cannot write: a DSM keyword as identifier, a forked or cyclic namespace, a non-literal default (
inf,nan), a duplicate parameter name, a non-identifier field or case name.A default equal to the type’s own default is stored as no default.
1.2.19 — 2026-07-06¶
Added
An XML wire format for values and DSM definitions:
XmlValueEncoder,XmlValueDecoder,XmlDSMDefinitionsEncoderandXmlDSMDefinitionsDecoder, beside JSON and BSON, on a vendored pugixml.
1.2.18 — 2026-07-03¶
Fixed
The JSON value decoder reads an integer literal into a
floatordoublefield:5is5.0, JSON having one number type. The encoder still writes5.0.
1.2.17 — 2026-06-28 (phantom)¶
No runtime change. The runtime number was bumped in lockstep with the dsviper Python wheel 1.2.17 (a binding bug-fix release) — the last lockstep bump before
viper_version()decoupled the wheel and runtime streams. Shipped unchanged by dsviper for Node.js 1.2.1 and 1.2.2.
1.2.16 — 2026-06-13¶
Added
A merge can be analysed and reconciled before it is written:
CommitStateBuilder::mergeStateandmergeEnabledByCommitIdcompute whatmergeCommit(ours, theirs)would give, without writing it.CommitMergeAnalyzer::analyzeVirtualMerge,reconcileStateandmaterializeMerge: analyse that state, compose the chosen values in memory, then write the merge and its reconciliation together.CommitMergeAnalysis::mergeCommitbecomes optional, empty for a virtual analysis.
Fixed
A document holding a set or a map builds its
DocumentNodetree: the node identity encoded paths through set elements and map entries, which the path writer refused.A path through a set element or a map entry is written and read back whole; the path codec handled regular paths only.
1.2.15 — 2026-06-11¶
Changed
Head navigation and state construction leave
CommitDatabase:reduceHeads,forwardandfastForwardmove toCommitDatabaseHelper,initialState,stateandenabledByCommitIdtoCommitStateBuilder, each taking the database.CommitDatabaseHelper::reduceHeads(db, anchor)merges the other heads into the head you name, refusing one that is not a head (CommitErrors::notAHead); without an anchor it starts fromlastCommitId.
Removed (breaking)
CommitStore::Instance(), the process-wide store. Build one withCommitStore::make()and own it.
Fixed
XArray::containsandpositionOfcompile for keys and structures, which define==only; they called anisEqualthose types lack.The runtime builds with MSVC again.
1.2.14 — 2026-06-10¶
Changed
A
Fuzzeris reproducible: one seeded generator drives every draw, the UUID family included, andseed()answers the seed, so a run can be replayed. A seed can be passed at construction.
1.2.13 — 2026-06-04¶
Added
StreamReading::remaining()andStreamRawReading::size(): how many bytes are left to decode, and how many the source holds.
Fixed
A malformed JSON or BSON document raises a
Viper::Error, value or DSM definitions; the parser’s own exception escaped the runtime.Hashing a shared value or id from two threads is safe: the hash was cached lazily under a
constmethod. Ids now hash at construction, strings and blobs once.CommitStore::Instance()initialises safely under concurrent first use.UUId::hash()no longer reads a misaligned integer, undefined behaviour on some platforms; the hash is unchanged.
1.2.12 — 2026-05-31¶
Changed
The Database ↔ CommitDatabase converters take open databases, local or remote, instead of file paths. The commit to read, or the label to write, is explicit; the result is a
DatabaseTransferInfo(documents, blobs) instead of text on standard output.A transfer copies only the blobs the copied state references, streamed in 64 MiB chunks, so a blob past 2 GB is carried.
Databasing::createBlob(blobId, layout, blob)takes the id and answers whether it created the blob, asCommitDatabasingdoes; it computed the id and returned it.
Added
DatabaseCopiercopies aDatabaseinto another one whole, orphan blobs included.CommitDatabaseFlattenercollapses one commit of aCommitDatabaseinto a newCommitDatabaseholding that state as its only commit, with the blobs it references.
1.2.11 — 2026-05-29¶
Added
CommitMergeAnalyzerreconstructs what a merge dropped:analyzeMergelists, per document, the paths where a branch’s change did not survive, andreconcilewrites the chosen values as one commit on top of the merge (CommitMergeAnalysis,CommitMergeDocument,CommitMergeConflict,CommitMergeResolution).
Fixed
Every decoder checks a key against its field’s type: the binary one accepted any concept. A key must name the field’s concept or a descendant, or a descendant of a club member (
notAConceptDescendant,notAClubMemberDescendant).The JSON decoder refuses a club key whose concept is not a member; the check was inverted and never raised.
The JSON value decoder names the failing node by its path, checks the shape of an xarray, and refuses a negative number for an unsigned type.
The binary Definitions decoder refuses a stored
runtimeIdthat does not match its type; four of its five checks built the error without raising it.
1.2.10 — 2026-05-11¶
Changed
CommitIdhashes the parent, the type, the target and the opcodes only — no longer the timestamp or the label, so replaying the same opcodes from the same parent gives the same commit. EveryCommitIdcomputed before changes.
Fixed (breaking)
lastCommitIdbreaks a timestamp tie by insertion order; SQLite answered either of two commits sharing a timestamp.
Fixed
reduceHeadsruns in an exclusive transaction: a head added between its read and its merges was left unreduced.
1.2.9 — 2026-05-08¶
Changed
A JSON DSM definitions decoding error names the failing node by its path, where it gave the kind of node only.
Fixed
The JSON DSM definitions decoder keeps
isMutable: it set every attachment function mutable, so a pure one turned mutable after a JSON round trip.
1.2.7 — 2026-04-16¶
Changed
NaN has a place in the total order: it equals itself, sorts before every other value, and every NaN hashes alike, so a set or a map key can hold one.
A map diff emits
MapUpdatefor a modified key, where it folded it intoMapUnion: the program reads Subtract, Update, Union, one opcode per kind of change.
Added
ValueDoubleandValueFloatcarryINF,NEG_INFandNAN, besideZEROandONE.make()answers these singletons for the special values.
Fixed
ValueFloataccepts ±inf and NaN: the range check that guards the narrowing from double refused them as overflow.An empty set is disjoint from itself: the shortcut for a set compared with itself answered false whether it was empty or not.
ValueMap::pop(key, default)answers the default on an empty map; it raised.CommitDatabase::isAncestorstays linear on a DAG with many merges: it walked shared ancestors once per path, exponential in the number of merges.DatabaseSQLite::delBloboutside a transaction is refused, as every other mutation is; it deleted.Service::makerefuses two pools with the same name or UUID: the duplicate check searched maps it never filled, so any duplicate passed.Definitions::createStructurerefuses a structure without fields (TypeErrors::EmptyStructure, 44), as the DSM checker does; one broke theto_dsmround trip.An enumeration or a variant may hold 256 cases, what a
uint8index addresses; the checker stopped at 255.BlobPackDescriptor::addRegionrefuses an empty name (NameEmpty, 2) and a count of zero (CountZero, 3).A blob pack whose region count is corrupt is refused before the size computed from it overflows.
A string holding a NUL byte is written whole by the binary, raw and hashing streams, which cut it at the first NUL.
StreamWriterFile::writereports a failed write (a full disk, a closed stream); the bytes were lost without a word.ValueXArray::disablePositionrefuses a position it does not hold; it recorded a tombstone for it.A path into an xarray reads the position it names:
isApplicableused the component’s rank as an index, reading another element or past the end.CommitStore::reseton an empty database, or without a notifier, does nothing; it dereferenced what was not there.SharedMemoryworks on Windows:createandopenswapped the handle and the address, so the first access crashed.An unset
HOMEno longer crashes the path helpers, which built a string from a null pointer.The runtime builds with GCC and links
librton manylinux, whereshm_openlives; the wheel failed to import there.
1.2.5 — 2026-03-24¶
Fixed
Extending the definitions of a
Databasekeeps what it held:DatabaseSQLitestored the new definitions alone instead of the merged ones.A path through nested collections pivots on its last entry or element:
isEntryKeyPath,isElementPath,entryKeyInfoandelementInfotook the first, and misread a set inside a map value.
1.2.0 — 2026-03-20¶
Initial release.
Type/Value system with reference semantics; Database engine (SQLite backend); Commit engine with content-addressable storage (SHA-1); blob storage with attachment support; JSON and binary stream codecs; RPC and network services; a single structured exception type.
Platforms: macOS 15, Windows 10/11, Linux Ubuntu 24.04 LTS (x86_64 / arm64).
dsviper for Python¶
The PyPI wheel (pip install dsviper). Its PATCH stream is independent of the
runtime; each release notes the runtime version it ships.
1.2.28 — 2026-09-27¶
Ships runtime 1.2.27.
Changed (breaking)
BlobArray(blob_layout, blob)replacesBlobArray.from_blob, andBlobArrayBuilder(blob_layout, count)replaces the removed allocating constructor.
Changed
A transfer runs once.
convert()andflatten()raise on a second call, socommit_id()is never ambiguous.from dsviper import *no longer re-exports the compiled submodule.Passing a non-Value where one is required raises
TypeErrorrather thanRuntimeError.
Added
ValueXArray.size()andBlobArray.data_count(), answering whatlen()answers, as every other container and blob array already did.TypeName(name_space, name)builds one, andrepresentation()/representation_in()render it qualified or short.DefinitionsInspector.is_ambiguous(attachment)says whether a short name still names one thing.BlobGetting.read_blob(blob_id, size, offset)reads a blob in pieces, including past 2 GB whereblob()refuses.FunctionPrototype.name()answers the name of the function it describes.DatabaseToCommitDatabaseConverter.commit_id()andCommitDatabaseFlattener.commit_id()answer the commit the transfer made, or None before it ran.BlobArrayBuilder.data_count()answers the range an index walks:count()timesblob_layout.components().Codec.queryandCodec.checkname the three stream codecs, say what each keeps, and which transport each belongs to. MixingSTREAM_RAWandSTREAM_BINARYcannot be reported: on a little-endian host they write the same bytes.The integer types state their range, and
help(dsviper)answers with what the package is.The type stub carries the binding’s prose, so an editor shows the same text as
help().The READMEs show undo and redo, a database on disk, the NumPy path, a commit pattern that no longer forks the history,
Databaseand the converters,parse()to anAttachment,ServiceRemote, and a pinned namespace uuid.
Fixed (breaking)
ValueMatis the sequence of columns it says it is.len(m)counts the columns, the indicesm[i]accepts;size()still counts the elements. A negative index counts from the end, andlist(m)gives the columns.
Fixed
Four stub declarations were wrong:
add_field(type=...)type-checked and raised, the keyword beingtype_or_value;memberships()returnsdict[ValueUUId, set[ValueUUId]];CommitData.data()returns aValueBlob;injectis onDefinitionsConst.Value.createis declared to return the class the type names, notValue: a type checker refusedValue.create(TypeVector(Type.STRING)).append(...), which runs.The stub no longer declares what the binding lacks:
BlobPackRegion.copyandServiceRemoteAttachmentFunctionPool.definitionsraisedAttributeError;TypeVector.castdeclared aTypeOptional.Valuestates the runtime’s total order: across types by kind, then type; a vector, a set or a map by size first, so[3, 1, 2]sorts after[4].A read that is a snapshot says so:
keys(),CommitStore.state(),attachment_getting(). AValueBlobcopies its bytes andencoded()returns a copy; aValueSetcopies its elements, aValueMapits keys.The container texts say what an index does:
at()raises on a negative index wherev[-1]counts from the end; a set element is addressed by position; a field is written as an attribute.The
CommitStoretexts say whatclose()releases, that an undo afteruse_commitopens a second head, thattimestamporders nothing, and that a merge drops one side unsignalled,CommitMergeAnalyzerreconstructing it.A dispatch says what it raises and what it notifies: it raises before running; a failure once running goes to the notifier and
dispatchreturns None. Nothing removes a key: set nil over an optional document.A name is an identifier and not a C++, Python or TypeScript keyword;
inject()spells names in upper snake case;KeyNamersays which attachment carries a display name; aDefinitionsInspectorgoes stale after a later create.The DSM texts say what they count:
DSMParseError.line()andpos()are 1-based,pos()in characters; a function pool is declared beside the namespace;from_function_pooltakes the runtime pool.The database texts say what they answer:
path()of a database in memory, that transactions do not nest, that'Deferred'is the default, and that an exclusively locked file waits 10 s, then raises.A transfer states its target and its failure: it writes into a freshly created database, a run that raises part-way keeps what it wrote, and an exception raised by the stepper is ignored.
The blob texts say what runs:
BlobEncoderLayout.type()andelement_type()were swapped,BlobLayout.data_type()returns a code, and a failed stream append or close deletes the stream.The codecs and services say what they do: XML keeps every value, JSON writes a uint64 bare;
settakes a regular path,patchthe others;ServiceRemote.connectextends the definitions it is given.ValueStructure.setwith an undeclared field raises the runtime’sViperError, asatdoes; it raised anIndexErrorof its own.An any or a variant compares with a native decoded to the type of the value it holds:
ValueAny(ValueInt32(5)) == 5and a variant holding"txt"equal to"txt"answered False. None against an any is the empty any.None as an input is read by the type it is decoded to: empty in an optional or an any, void in a void slot, no default where the type cannot hold it.
ValueMap.get(key, None)no longer raises,pophonours a None default, and None decoded into an any is empty, notAny(void).ValueTuple.at,ValueMat.atandValueMat.setraise IndexError on a negative index, as their texts and every other container read do; they raised OverflowError.ValueVoid.encoded()returns None, asValue.dumpsdoes; it returned aValueVoid, the one primitive whoseencoded()disagreed withdumps.A call on a closed
ServiceRemoteraisesViperError; it raised a bareExceptionnaming the client’s own, empty, address.The stub declares
ServiceRemote.poolsandattachment_poolsas properties, which they are: a type checker refusedservice.pools.Tools, the route their own text teaches.A
Pathand aPathConstwith the same components compare equal, both ways. They carry the same path;==answered False across the two.SharedMemory.fd()returns the file descriptor; it returned the region’s size.StepperDelegate.step(action, percent)checks its arguments, as its signature says; the default accepted anything.DocumentNodestates the fifteen kindstype()answers, whatstring_value(),string_component()andstring_value_tooltip()show for each, and that a uuid or a blob id is primitive.ValueKey.detail_type_representation(),BlobPack(descriptor),Attachment.description()and the passiveSocketfactories say what they do.Semaphore.wait()says that it holds the interpreter lock, so only another process can end it.The stub lets
collect_blob_ids/collect_commit_idstake the Path, CommitState, CommitMutableState or ValueProgram they document; it declared onlyValue.is_compact,is_sizedandpack_sizedsay what they are: compact is bool-or-number fields, sized is fixed and readable alone (not a key), a pack-sized vector hands out copies.concept_memberssays what it answers: the concept and its descendants, not its clubs; the source-map span docs say where each span starts and stops.A parameter the stub declares
X | Nonetakes None, as the signature says: everydocumentation,stream_codec_instancingandhashingargument,create_key,representation,description,byte_count,ValueXArray.insert. They raisedTypeError.Fuzzer.set_blob_id(None)gives each blob its own id again, as documented;Nonewas refused.An error raised by the caller’s own object reaches the caller. A sequence whose
__getitem__raised crashed the interpreter; one whose__len__raised, or a string that cannot be encoded (a lone surrogate), ended inSystemErroror a misleading error.A
str,bytesorbytearrayis not a sequence of elements: a vector, set, tuple or xarray built from one is refused, as in Node;"abc"became['a', 'b', 'c']. A set from a non-iterable raisesViperErrorinstead ofSystemError.NameSpacerefuses an invalid name or uuid withValueError; it raisedRuntimeError.An enumeration case is read one way everywhere:
case,.caseorEnum.case, the enumeration named; the constructor acceptedEnum.case.extraand refused.case,loadstook any name. A string that is not base64 names its path.A deduce that cannot read its object has a code of its own (27); it shared an unrelated one. The empty-case message is well formed.
ViperErrornames the three layers a call crosses and what each raises; it said a wrong type raisesTypeError, which a native that does not fit its type does not.The
CommitStoreoperations say what they do to the undo stack;dispatch_diffsays whatrecursivewalks;is_closedand the*_speedmeasures say what they measure.CommitData.blob_idsanswers the empty set for a commit without mutations; it raised.need_transmitandsyncsay they readdata_version(), which a write through the synchronized connection does not move.A notifier that raises no longer fails the store call with
SystemError. Its exception is dropped, as for a logger or a stepper: the store has already acted.sync_datano longer promises the blobs its commits reference; they travel throughblob_datas.blob()andcreate_zero_blobsay that a reserved blob raises untilfreeze_blobseals it;blob()promised None.begin_transaction(None)is accepted onDatabasingandCommitDatabasing, as the stub declares; it raisedTypeError.help()no longer shows a phantom first parameter on a static method. All 202 readfrom_index(module, /, index)orcast(self, /, value); they now readfrom_index(index).A negative index or size raises instead of wrapping. It was read modulo 264, so
position(-264)answered the first position andPath.from_index(-1)built a path to index 18446744073709551615.Float16.to_floatrefuses bits past 16.SQLite.get_pragmadeclares its key astr, andruntime_idon an attachment and on an opcode key no longer calls it a type. Three function classes said nothing in the package hands one back; their pool does.ValueXArray[i]follows Python’s own indexing:IndexErrorpast the end, a negative index counting from the end, andKeyErrorfor a position the array does not hold. It answeredNoneto all three.PathConst.encodenamed the wrong codec. It documentedSTREAM_TOKEN_BINARYwhere it andPath.decodeboth useSTREAM_BINARY, so the round trip it describes would not have worked as written.StreamCodecInstancing.name()said the name travels in the stream. It does not: a stream carries no record of which codec wrote it.chunked()said a chunked blob reads like any other. Past 2 GB it is written withblob_stream_create/_append/_closeand read withread_blob;blob()refuses it.DSMSourceSpanoffsets are characters, start 0-based and stop inclusive, asDSMParseError.pos()counts them; andDSMAttachment.identifiercited two calls that class does not have.Seventeen static methods were documented
$self-Error.parse,Path.from_unwrap,diff_keysand the fourteencast- andfront,back,pop_maxanddocuments_detailsnamed the wrong default.get()andenumerate()hand back a copy, which only the write side stated;StepperDelegate.steptakespercentas a fraction from 0.0 to 1.0.FunctionPool.funcsis a property, which its class described as a call.blob()past 2 GB raises, which its-> ValueBlob | Nonesignature did not say; the message now namesread_blob.Three classes could segfault the interpreter from pure Python, and three methods were uncallable or refused an argument they document.
25 constructors named a keyword the binding does not accept, and five classes promised what they do not do.
get()raises on a key of another concept, whichis_nil()does not report;create(),open()andset()now state their preconditions.nodes()is keyed byValueCommitId, not a uuid, and holds one entry more than there are commits: the root the layout starts from.commit_ids()answers a set, in no order to rely on; the undo label isUndo [...]andcommit_type()is what answersDisable.is_equalcompares content, so two registries built apart from the same model are equal, andhexdigest()answers the same question on a string.Value.createwraps and the constructor copies shallowly,Definitions.const()is a live view where a store’sdefinitions()is a snapshot, and crossing a store copies.runtime_idis computed, not assigned, and from three different things depending on what carries it; it was documented as “the uuid assigned by the runtime” on 40 methods.An out-of-range integer names the type you asked for, and the docstrings no longer name classes the package does not have.
The transaction rule is stated on
Databasing, the class a caller meets, and the stub says what anencodedgetter hands back.NameSpacesays its uuid is the model’s lasting identity, which is what lets a later run read what an earlier one wrote.to_dsm()says how its output is arranged — types sorted, then the mapping, per attachment.The package says what a
.dsmlooks like, and can render your own definitions back as DSM source.reconcile_statesays where it applies: over a materialized merge, and only there.Definitions.create_concept,create_clubandcreate_attachmentsay where the documentation they take ends up.Every class in the stub carries its summary, and two declarations that disagreed with the binding were corrected.
The relations comment no longer overstates what is total.
PathConst.patchsays which two shapes address a key, an element of a set and a map entry’s key, where it described an entry of a set and sent a reader into a raise.TypeMatrefuses a dimension of zero, and a negative one, which it read as unsigned and built into a matrix of 18446744073709551615 columns. Both it andTypeVectake a dimension as wide as a count, and state their bound.
1.2.27 — 2026-09-20¶
Changed
The blob readers no longer write.
BlobArrayandBlobPackRegionlost their writable paths: writing into a sealed value would change what itsBlobIdnames.A
BlobPackis no longer declared aMapping, having none ofkeys,itemsorvalues;pack['absent']now raises whatcheck('absent')raises.AttachmentMutatingis anAttachmentGetting, which the binding always built and the stub did not declare.A remote database holds a blob twice at most while it crosses the wire, not three times, and a blob read from a store is copied once less.
Removed (breaking)
BlobArray(blob_layout, size)— it allocated a blob of zeros whose hash andBlobIdwere computed on those zeros. UseBlobArrayBuilderto fill,BlobArray(layout, blob)to read.BlobArray.__setitem__,BlobPackRegion.__setitem__andBlobPackRegion.copy()— they wrote into a blob shared with whoever held the value.
Added
py.typed— mypy and Pyright now check your code against the binding’s real surface.BlobArrayBuilder(blob_layout, count)fills the bytes of a blob, then seals them.AttachmentMutating.attachment_getting()reads back what a mutable state holds.encoded=Trueon the fourteen reads that projected to a native with no way back.DSMTypeandDSMLiteralare declared, with their subclasses.
Fixed
Nothing writes into a
ValueBlobany more, and a read-only buffer refuses a writable request.The class hierarchy the type hints declare is the one the binding builds, the protocols it answers are declared, and
copyis typed by its receiver.The type hints say what a projecting read returns, accept what the binding accepts, and a read that can answer
Nonesays so where one that cannot does not.AttachmentMutating.setanddiffrefuse a document of another type, and a merge resolution keeps the type of its locus.A namespace cycle closed through an attachment or a key is rejected.
A remote database no longer reserves the memory a peer announces but never sends, and
CommitDatabaseRemote.upload_speed/download_speedreport the direction they measure.ValueMap.setdefaultreturns the value,ValueMatsays it is column-major, andCommitSynchronizer.sync()takesNonefor its logging.
1.2.26 — 2026-09-05¶
Ships runtime 1.2.25.
Changed (breaking)
Seven constructor keywords are renamed, and a caller passing one by keyword breaks:
CommitDatabaseServer(database_path=),ValueString,ValueUInt16andValueBool(initial_value=),TypeVecandTypeMat(numeric_type=),DefinitionsMapper(source_…, target_…).
Changed
The docstrings say what each class and method does, rewritten against the C++ they wrap; the stream writers state which Python types each accepts and converts.
The docstrings state the decided behaviours:
ValueSetandValueMapiterate sorted, NaN equals itself and sorts below-inf, andValue.dumps’jsonflag changes two things.
Fixed
Eight stub returns admit
None:DSMConcept.parent,DSMStructureField.default_value, andblob/blob_infoonCommitDatabase,DatabaseandBlobGetting.get()is documented as returning aValueOptional, always; the text saidNone, so aget(...) is not Nonetest was always true.Four signatures render as signatures in
help(), and three stream constructors no longer claim to take no argument.SocketandStreamWritingdescribe themselves, notSharedMemoryand a reader;DSMTypeMatdescribes a matrix, and three classes state their instantiability correctly. Ships runtime 1.2.25.
1.2.25 — 2026-08-27¶
Ships runtime 1.2.25.
Fixed
Type.representation()of a tuple or a variant no longer depends on call order, nor races between the client threads of aCommitDatabaseServer.ValueXArray.rebuild_fromrefuses a source of the wrong type with CPython’s ownTypeError. Ships runtime 1.2.25.
1.2.24 — 2026-08-04¶
Ships runtime 1.2.24.
Added
CommitDatabaseServer.finish_before(timeout_in_sec)bounds the teardown and returns how many client threads it could not join;step(timeout_in_sec)is a bounded wait, soSIGINTis handled between steps.Socket.close()andSocket.is_closed(): a passive local socket is a file, which a host can now release.
Fixed
CommitDatabaseServerrefuses a logger that calls back into Python (LoggerPrint, one built withLogging.create) with aTypeError; its client threads run outside the GIL and would corrupt the interpreter.
Packaging
A source file added to the runtime is picked up without re-running CMake. Ships runtime 1.2.24.
1.2.23 — 2026-07-23¶
Ships runtime 1.2.23.
Fixed
AttachmentGetting.geton aCommitStatereturns a document its cache does not share: mutating a first read changed every later read of that key. Ships runtime 1.2.23.
1.2.22 — 2026-07-22¶
Ships runtime 1.2.22.
Added
DSMSourceMap:DSMBuilder.parse(source_map=DSMSourceMap())records the source span of every declaration, field, case, namespace, type and resolved reference.
Fixed
CommitMergeResolution.chosenreturns a copy: it handed out the stored decree itself, which a caller could then change. Ships runtime 1.2.22.
1.2.21 — 2026-07-20¶
Ships runtime 1.2.21.
Changed
A
ValueStringmust be valid UTF-8, and documentation cannot contain"""; both were accepted before.
Fixed
A string holding a NUL byte crosses the binding whole; it was cut at the first NUL.
The HTML renderer escapes names, strings and documentation. Ships runtime 1.2.21.
1.2.20 — 2026-07-13¶
Ships runtime 1.2.20.
Added
Value.collect_commit_ids(value, type, definitions)andType.use_commit_id(type), the commit-id twins ofcollect_blob_idsanduse_blob_id.ValueXArray.items(encoded=...), as onValueMap:encoded=Falseyields the typed elements.ValueXArray.rebuild_from(source, ...)copies a source xarray’s positions and tombstones and installs new elements in one step.
Fixed
extend_definitionswith a type redefined under a newruntimeIdis refused before any write; it left a database that no longer opened.A
DSMDefinitionsbuilt through the binding is refused where the DSM could not write it: keywords as names, forked or cyclic namespaces, non-literal defaults, duplicate parameters. Ships runtime 1.2.20.
1.2.19 — 2026-07-06¶
Ships runtime 1.2.19.
Changed
The third-party notices list pugixml, now linked into the wheel. Ships runtime 1.2.19.
Added
Value.to_xml_string(value, indent=...),Value.from_xml_string(string, type, definitions),DSMDefinitions.to_xml_string(indent=...)andDSMDefinitions.from_xml_string(string).
1.2.18 — 2026-07-03¶
Ships runtime 1.2.18.
Changed
The documentation link points at the
dsviper-pythonlanding page. Ships runtime 1.2.18.
Fixed
==and!=between values never raise: they converted the other operand to the receiver’s type and raised on a mismatch. Two values now compare in the runtime’s total order; a native that does not fit compares unequal.ValueAnyandValueVariantcompare symmetrically: an any equalled its raw content while the content did not equal the any. An any now equals an any only; unwrap it to compare the content.Type.ANY_CONCEPT == Type.ANY_CONCEPTisTrue: the comparison used theTypeAnysingleton, so it also equalledType.ANY.
1.2.17 — 2026-06-28¶
Ships runtime 1.2.17.
Added
viper_version()answers the runtime the wheel embeds, apart fromversion(), the wheel’s own; from here the wheel’s patch number moves on its own.
Fixed
ValueBlobId.encoded()returns astr, asValueCommitIdandValueUUIddo; it returned aValueBlobId.TypeMap.values_type()is a vector of the elements; it answered the key set.StreamReaderSharedMemory.size(),ValueOptional.hash()andValueEnumeration.hash()exist; the stub declared them and calling them raisedAttributeError.The stub states
ValueOptional.unwrapandgetdefault toencoded=True, as they do, and thatCommitStore.dispatchreturns the callable’s result.The stub no longer declares
CommitData.transcodeorDSMParseError.part, which exist nowhere.The README’s example builds states through
CommitStateBuilder, as 1.2.15 requires. Ships runtime 1.2.17.
1.2.16 — 2026-06-13¶
Ships runtime 1.2.16.
Added
CommitStateBuilder.merge_state,merge_enabled_by_commit_id, andCommitMergeAnalyzer.analyze_virtual_merge,reconcile_state,materialize_merge: a merge analysed and reconciled before it is written.
Fixed
DefinitionsExtendInfo.memberships()maps each club to its members; it mapped each club to itself.DefinitionsInspector.check_attachmentreads its identifier: a wrong argument format made every call undefined.blobanddel_blobrefuse ablob_idthat is not aValueBlobIdwith aTypeError; they read any object as one and crashed.An error inside
DefinitionsConst.injectordiscard, a tuple’sreprorlen, or an xarray’s iteration becomes a Python exception; it escaped into the interpreter.Returned tuples, dicts and map items no longer leak: some twenty sites kept a reference per element. Ships runtime 1.2.16.
1.2.15 — 2026-06-11¶
Ships runtime 1.2.15.
Changed
CommitDatabase.forward,fast_forward,reduce_heads,initial_state,stateandenabled_by_commit_idare gone: useCommitDatabaseHelperandCommitStateBuilder, which take the database first.CommitDatabaseHelper.reduce_heads(commit_database, commit_id=None)merges the other heads into the one you name.
Removed (breaking)
CommitStore.instance(): build aCommitStore()and keep it. Ships runtime 1.2.15.
1.2.14 — 2026-06-10¶
Ships runtime 1.2.14.
Added
Fuzzer(definitions, seed=...)andFuzzer.seed(): a run replays from its seed. Ships runtime 1.2.14.
1.2.13 — 2026-06-04¶
Ships runtime 1.2.13.
Added
remaining()on the stream readers andsize()on the raw readers.
Fixed
A stream read over a temporary
ValueBlobreads valid memory: the reader did not keep the blob alive, and read freed bytes. Ships runtime 1.2.13.
1.2.12 — 2026-05-31¶
Ships runtime 1.2.12.
Changed (breaking)
Databasing.create_blob(blob_id, blob_layout, blob)takes the id and returns abool, whether it created the blob, asCommitDatabasingdoes; it returned the computed id. Ships runtime 1.2.12.
Added
DatabaseToCommitDatabaseConverter,CommitDatabaseToDatabaseConverter,DatabaseCopierandCommitDatabaseFlattenermove documents and blobs between the two stores, each answering aDatabaseTransferInfo.StepperDelegate, subclassed in Python, receives the progress of a transfer.
1.2.11 — 2026-05-29¶
Ships runtime 1.2.11.
Added
CommitMergeAnalyzer,CommitMergeAnalysis,CommitMergeDocument,CommitMergeConflictandCommitMergeResolution: what a merge dropped, per document and per path, and the commit that writes the chosen values back.
Fixed
A key built from Python natives is checked against its field’s type, as the runtime’s decoders now do: its concept must be the field’s or a descendant. Ships runtime 1.2.11.
1.2.10 — 2026-05-11¶
Ships runtime 1.2.10 (content-addressed
CommitIdnarrowed — breaking;reduceHeadsatomicity;lastCommitIdtie-break).
1.2.9 — 2026-05-08¶
Ships runtime 1.2.9.
Changed
An editable install rebuilds incrementally on import (
editable.rebuild, a persistent build directory). Ships runtime 1.2.9.
1.2.8 — 2026-05-03¶
Changed
Packaging — PEP 639 license metadata:
LICENSE+THIRD-PARTY-NOTICES.txtembedded underdist-info/licenses/;License-Expression: LicenseRef-DigitalSubstrate-Commercial-1.2(the deprecated proprietary classifier removed).Ecosystem — runtime/DevKit split: the viper repo scopes to the runtime (
src/Viper,src/P_Viper,dsviper_wheel); DevKit content extracted to standalone repos. README rewritten around the runtime-only scope;requirements.txtdropped.
1.2.7 — 2026-04-16¶
Ships runtime 1.2.7.
Changed
The wheel builds with scikit-build-core (PEP 517), configured in
pyproject.toml;setup.pyandbuild.pyare gone.
Added
Wheels for Linux, macOS and Windows are built, tested and published by CI — five Python versions per platform, a release candidate to TestPyPI first; publication was manual.
ValueDouble.INF,NEG_INF,NAN, and the same onValueFloat.
Fixed
ValueString(None)is the empty string, asValueBool,ValueInt*andValueDoublealready default onNone.ValueFloataccepts a Python float that is ±inf or NaN.A set the binding returns no longer leaks: every
wrapSetOf*kept one reference per element.A buffer is released when decoding it fails;
decodeBlobBufferreleased it on success only.A C++ exception of any type becomes a Python error: one not derived from
std::exceptioncrossed the C API boundary, which is undefined.Ten return types in the stub were wrong:
Definitions.extend,extend_concepts,ValueVoid.encoded,CommitDatabase.reduce_headsandSharedMemory.unlinksaidNone, as did the in-place set and map operators.PyPI lists all three supported operating systems; each wheel advertised its own build machine’s.
Wheels build on manylinux, which ships no
libpython: CMake asked forDevelopmentinstead ofDevelopment.Module.
1.2.5 — 2026-03-24¶
Ships runtime 1.2.5.
Fixed
CommitStore.dispatchreports a failing Python callback throughnotify_dispatch_errorinstead of raising, as the C++ store does;notify_erroris renamed to match.DefinitionsConst.injectanddiscardtake an optional namespace, a dict or a module, so an embedded editor evaluating in its own globals sees the constants; they wrote into__main__.Every wheel carries its long description and full metadata. Ships runtime 1.2.5.
1.2.0 — 2026-03-20¶
Initial release.
Strong-typed Python C-API binding with seamless conversion; native Python collections accepted as input (metadata-driven). Published on PyPI (
pip install dsviper).Ships runtime 1.2.0.
dsviper for Node.js¶
The npm package @digitalsubstrate/dsviper. See dsviper for Node.js.
1.2.13 — 2026-09-27¶
Ships runtime 1.2.27.
Changed (breaking)
A stream array read answers a
ValueVec, as in Python:readUint8s(n)and its nine siblings return avec<T, n>;toArray()gives the native array.BlobArrayreads binary.at()answers one datum as a native number (a bigint for the 64-bit types) and iteration walks the data;blobView()is the element reading, andtoTypedArray()still hands out the whole run.new BlobArray(blobLayout, blob)replacesBlobArray.fromBlob.
Changed
An array write takes a
ValueVec, as declared, and refuses a sequence that does not holdsizeelements; it ignoredsize.A native that does not fit its type throws
ViperError, stream writers included, naming the type expected, what was given and the path, as Python does. A signature argument still throwsTypeError.Every optional parameter declares
| null. The binding has always taken null as absent — each optional argument is read through a type guard — and the declarations now say so, which matters because JSON has noundefined.A transfer runs once.
convert()andflatten()throw on a second call, socommitId()is never ambiguous.The third-party notices list what the package embeds, and nothing else.
Added
Semaphore, the named semaphore the wheel already had:create,open,exists,unlink,tryWait,waitandpost.waitblocks the event loop;tryWaitpolled betweensetImmediateturns waits without blocking it.ViperErroris a class, exported: a runtime refusal isinstanceof ViperErrorand carriescode,componentanddomain.namestays'ViperError'; an index past the end stays aRangeError. It names the three layers a call crosses.A method taking ids takes a
ValueSetas well as an array —blobInfos,blobDatas,commitDatas,syncData,unknownBlobIds— so what a query answers can be handed back.new TypeName(nameSpace, name)builds one, andrepresentation()/representationIn()render it qualified or short.DefinitionsInspector.isAmbiguous(attachment)says whether a short name still names one thing.BlobGetting.readBlob(blobId, size, offset)reads a blob in pieces, including past 2 GB whereblob()refuses.FunctionPrototype.name()answers the name of the function it describes.DatabaseToCommitDatabaseConverter.commitId()andCommitDatabaseFlattener.commitId()answer the commit the transfer made, or undefined before it ran.BlobArrayBuilder.dataCount()answers the lengthcopy()requires:count()timesblobLayout.components().Codec.queryandCodec.checkname the three stream codecs, say what each keeps, and which transport each belongs to. MixingSTREAM_RAWandSTREAM_BINARYcannot be reported: on a little-endian host they write the same bytes.The twelve constants the declarations promised now exist, and the README names the entry points a reader could not find.
Fixed
CommitData.data()is declared to return aValueBlob, which it always returned; the declaration saidCommitData.A thrown
TypeErrornames parameters a JS caller can type:blob(blobId), notblob(blob_id). The messages printed Python’s snake_case, and two printedseed=None.Valuestates the runtime’s total order: across types by kind, then type; a vector, a set or a map by size first, so[3, 1, 2]sorts after[4].keys()andCommitStore.state()are snapshots: a later write or dispatch does not reach them. A blob copies its bytes in, andencoded()returns a copy.ServiceRemote.connectextends the definitions it is given, so an emptynew Definitions()suffices; the port has no default. The README names the client half.runtimeIdis computed, not assigned:Definitionssays from what for each kind, andNameSpacethat its uuid is the model’s lasting identity, the invalid one beingGLOBAL’s.A name is an identifier and not a C++, Python or TypeScript keyword;
inject()spells names in upper snake case;KeyNamersays which attachment carries a display name.The
CommitStoretexts say whatclose()releases, that an undo afteruseCommitopens a second head, thattimestamporders nothing, and that a merge keeps the later side unsignalled,CommitMergeAnalyzerreconstructing it.reconcileStatesays where it applies: over a materialized merge. Committing it over a virtual merge state adds a third head;materializeMergeis the route.A transfer states its target and its failure: it writes into a freshly created database, a run that throws part-way keeps what it wrote, and a throwing progress callback is ignored.
The database texts say what they answer:
path()of a database in memory, that transactions do not nest, that'Deferred'is the default, and that a file locked by another process waits 10 s, then throws.The codecs say what each keeps: XML carries every value,
toJsonStringwrites a uint64 bare and an enumeration as.case, anddecodeneeds the type, the definitions and the codec from elsewhere.Both READMEs run as written: a
Databasesample, the path fromparse()to anAttachment, a pinned namespace uuid, anddefinitions.const().toDsmDefinitions(), which threw withoutconst().ValueStructure.setandsetInwith an undeclared field throw the runtime’sViperError, asatdoes; they threw a plainErrorthatError.parsecould not read. A field name that is not a string throwsTypeError.CommitStore.dispatchis declared to returnT | undefined: a callable that throws commits nothing and answers undefined, its error going to the notifier. The class says what a dispatch throws and what it notifies.An any or a variant compares with a native decoded to the type of the value it holds:
equals(5)on an any holding int32 5 answered false. null against an any is the empty any.A blob size or offset is checked:
readBlob,blobStreamCreateand the other blob sizes, andSharedMemory.create/open, throw on NaN, a negative or a fraction. A negative or NaN offset read from offset 0; a NaN size opened an empty stream.A bigint beyond int64 decodes to the double it names; it became 0.
A boolean flag of another type throws
TypeError:readonlyofopen,jsonofdumps,packSized,recursiveofdispatchDiff,showTypeofdocumentsDetailsandenabledofdispatchEnableCommitcoerced any value, soopen(path, 'yes')opened read-only.Null as an input is read by the type it is decoded to: empty in an optional or an any, void in a void slot, no default where the type cannot hold it.
ValueMap.get/pophonour a null default, and a void slot accepts null, as its class says.ValueVoid.encoded()returns null, asValue.dumpsdoes; it returnedundefined, the one primitive whoseencoded()disagreed withdumps.A call on a closed
ServiceRemotethrowsViperError; it threw a plainErrornaming the client’s own, empty, address.ServiceRemoteAttachmentFunction.calldeclares theAttachmentMutatingit takes first: a type-checked caller could not write the call its class text describes.PathConst.equalstakes aPath, asPath.equalstook aPathConst: the two with the same components are equal both ways. It answered false one way and true the other.SharedMemory.fd()returns the file descriptor; it returned the region’s size.DocumentNodestates the fifteen kindstype()answers, whatstringValue(),stringComponent()andstringValueTooltip()show for each, and that a uuid or a blob id is primitive.ValueKey.detailTypeRepresentation(),new BlobPack(descriptor),Attachment.description()and the passiveSocketfactories say what they do.collectBlobIds/collectCommitIdstake a Path, CommitState, CommitMutableState or ValueProgram, as documented; they refused all but a Value.TypeKey.comparetakes any Type, as declared; it refused a type that is not a key.ValueVectorIter,ValueSetIterandValueTupleIterare exported, as declared.isCompact,isSizedandpackSizedsay what they are; a pack-sized vector hands out copies.conceptMemberssays what it answers: the concept and its descendants, not its clubs; the source-map span docs say where each span starts and stops.The
CommitStoreoperations say what they do to the undo stack;dispatchDiffsays whatrecursivewalks;isClosedand the*Speedmeasures say what they measure.CommitData.blobIdsanswers the empty set for a commit without mutations; it threw.needTransmitandsyncsay they readdataVersion(), which a write through the synchronized connection does not move.CommitStoreNotifying.createrefuses an object missing a notify method with aTypeError, as the declaration says and Python does; it accepted any object.SQLite.compileOptions()answers[name, value]pairs, as its declaration states; it answered a plain object.NameSpacerefuses an invalid name or uuid withTypeError, as the other signature checks do; it threw a plainError.An enumeration case is read one way everywhere:
case,.caseorEnum.case, the enumeration named. A string that is not base64 names its path.syncDatano longer promises the blobs its commits reference; they travel throughblobDatas.blob()andcreateZeroBlobsay that a reserved blob throws untilfreezeBlobseals it;blob()promised undefined.An optional argument of another type throws instead of being dropped.
undefinedandnullstill mean the default;createConcept(ns, name, parent)no longer creates a parentless concept.keys()is declared aValueSet<ValueKey>, and a structure’s field map takesundefinedas the runtime does, likenull: a typed caller no longer casts either.A second copy of the package in one process is refused, naming both. Two copies of the native binary do not recognise each other’s objects; loading both failed with an unrelated message.
Five declarations refused code that runs: a
ValueSetbuilt from ids,createBlobFromBuffergiven a typed array orArrayBuffer, abigintseed or pack size, and a progress callback.ValueOpcodeKeycompares by value andBlobPackRegionreads a datum, as Python’s==,hash()andregion[i]do:equals,compare,hashKey()andat(index).ValueMatiterates its columns, each a native array of its rows, as Python’s does. Spreading one threw.ValueXArray.size()andBlobArray.dataCount()count whatlen()counts in Python. Neither array could be counted:positions()includes END, andBlobArrayhad to be iterated.An index, a size or a count is an integer, or the call throws. NaN was read as 0 and 1.5 as 1, so
set(NaN, v)overwrote element 0.position()andPath.fromIndex()refuse a negative index.SQLite.getPragmatakes a string, as the runtime always did: it was declared taking aValueKey, which refused the one call that works.runtimeId()on an attachment and on an opcode key no longer calls it a type.TypeNameandNameSpacehave ahashKey()that followsequals. Keyed by their text, a native Map or Set merged two namespaces sharing a name and split a renamed one.A set of ids comes back as a
ValueSet, not an array —blobIds(),commitIds(),headCommitIds()and 33 others. An array had no membership by value:includes()compares identity, andsome()scanned. A spread gives the array back.Iterating a
ValueSetis linear. It walked the set from the start at every step.A stray property on a value is refused instead of shadowing the field:
s.name = 'Alice'answered on read while the field stayed empty. Every wrapper is sealed; strict code throwsTypeError, a sloppy script drops the write.PathConst.encodenamed the wrong codec. It documentedSTREAM_TOKEN_BINARYwhere it andPath.decodeboth useSTREAM_BINARY, so the round trip it describes would not have worked as written.StreamCodecInstancing.name()said the name travels in the stream. It does not: a stream carries no record of which codec wrote it.chunked()said a chunked blob reads like any other. Past 2 GB it is written withblobStreamCreate/Write/Closeand read withreadBlob;blob()refuses it.DSMSourceSpanoffsets are characters, start 0-based and stop inclusive, asDSMSourcePositioncounts them; andDSMAttachment.identifiercited two calls that class does not have.get()andenumerate()hand back a copy, which only the write side stated, on all four classes that expose them.ValueStringdeclaredhash()twice, andCancelationnow says the flag latches and thatcancel()is safe from a signal handler.A bad stream-codec argument segfaulted the process. It is now refused with a
TypeError, and an absent one takes the default.DatabaseTransferInfo.blobs()was declaredbigintand returnsnumber, andTypeVector.castdeclared the wrong class.47 docstrings said “the set of” and returned an array, and four classes carried the same false promises as their Python twins.
index.d.tsdid not compile for a consumer without@types/node, and cited names Node does not have.nodes()is keyed byValueCommitId, not a uuid, and holds one entry more than there are commits: the root the layout starts from.get()throws on a key of another concept, whichisNil()does not report;create(),open()andset()now state their preconditions.isEqualcompares content, so two registries built apart from the same model are equal, andhexdigest()answers the same question on a string.Value.createwraps and the constructor copies shallowly,Definitions.const()is a live view where a store’sdefinitions()is a snapshot, and crossing a store copies.An out-of-range integer names the type you asked for, and the undo label is
Undo [...]wherecommitType()is what answersDisable.The transaction rule is stated on
Databasing, the class a caller meets, and the README samples type-check undertscand still run.toDsm()says how its output is arranged — types sorted, then the mapping, per attachment — and the package says what a.dsmlooks like.ServiceRemoteFunctionPoolFunctionsdocumented a path this binding does not have, andServiceRemoteAttachmentFunctionPool.definitionsis gone from the declarations.Definitions.createConcept,createClubandcreateAttachmentsay where the documentation they take ends up.PathConst.patchsays which two shapes address a key, an element of a set and a map entry’s key, where it described an entry of a set and sent a reader into a throw.TypeMatrefuses a dimension of zero, and a negative one, which it read as unsigned and built into a matrix of 18446744073709551615 columns.TypeVecandTypeMatnow state their bound.
1.2.12 — 2026-09-20¶
Changed
The blob readers no longer write.
BlobPackRegionlostcopy(): writing into a sealed value would change what itsBlobIdnames.AttachmentMutatingis anAttachmentGetting, which the binding always built and the declarations did not say.A remote database holds a blob twice at most while it crosses the wire, not three times, and a blob read from a store is copied once less.
Removed (breaking)
new BlobArray(blobLayout, size)— it handed out a blob of zeros to be written into, and on this side nothing could reach those bytes. UseBlobArrayBuilder.BlobPackRegion.copy(buffer)— it wrote into a blob shared with whoever held the value.
Added
A set-typed input takes a JS
Set, alongside the array it already took.new BlobArrayBuilder(blobLayout, count)fills the bytes of a blob, then seals them.AttachmentMutating.attachmentGetting()reads back what a mutable state holds.encodedon the reads that projected to a native with no way back.
Fixed
Nothing writes into a
ValueBlobany more, and a host class can no longer derive from a bound one.index.d.tssays what Node answers: a subclass inherits its base’s statics, a read that can answerundefinedsays so,value.compare(other)declares the native it takes, andnew ValueBlob(value)declares what it takes.A 64-bit integer never takes a rounded value in silence, and a Value passed where a type is in scope is checked against it.
AttachmentMutating.setanddiffrefuse a document of another type, and a merge resolution keeps the type of its locus.A namespace cycle closed through an attachment or a key is rejected.
A remote database no longer reserves the memory a peer announces but never sends, and
CommitDatabaseRemote.uploadSpeed/downloadSpeedreport the direction they measure.new ValueSetandnew ValueMaptake whatValue.createtakes, andCommitSynchronizer.sync(logging)passes the logging on.
1.2.11 — 2026-09-05¶
Ships runtime 1.2.25.
Changed
Node 24 is the floor (
engines: ">=24", was>=18);[Symbol.dispose]is installed on the thirteen resource handles unconditionally.The JSDoc describes JavaScript: it had been copied from the Python docstrings,
True,None, snake_case names and “seamless with a Python int” included.The JSDoc states the decided behaviours:
ValueSetandValueMapiterate sorted, NaN equals itself and sorts below-Infinity, andValue.dumps’jsonflag changes two things.
Fixed
index.d.tsdeclares ten members the binding has: the surface ofDatabaseTransferInfo, andequals/compareonNameSpace,TypeName,BlobLayout,PathandPathConst.69 JSDoc said
nullwhere the method answersundefined. Ships runtime 1.2.25.
1.2.10 — 2026-08-27¶
Ships runtime 1.2.25.
Fixed
type.representation()of a tuple or a variant no longer depends on call order, nor races between the client threads of aCommitDatabaseServer. Ships runtime 1.2.25.
1.2.9 — 2026-08-04¶
Ships runtime 1.2.24.
Added
CommitDatabaseServer,SocketandCancelation: a Node host can serve a commit database.step(timeoutInSec)is a bounded wait, andfinishBefore(sec)answers how many client threads are still running.Socket.close(),Socket.isClosed()and[Symbol.dispose]: a passive local socket is a file, which a host can now release. Ships runtime 1.2.24.
1.2.8 — 2026-07-23¶
Ships runtime 1.2.23.
Fixed
AttachmentGetting.geton aCommitStatereturns a document its cache does not share: mutating a first read changed every later read of that key. Ships runtime 1.2.23.
1.2.7 — 2026-07-22¶
Ships runtime 1.2.22.
Changed (breaking)
ValueMap.items()returns an array of[key, value]pairs of handles, andkeys()/values()the wrapped elements;itemsreturned aValueVectorof tuples, soitems(false)gave natives.
Added
DSMSourceMap:DSMBuilder.parse(sourceMap)records the source span of every declaration, field, case, namespace, type and resolved reference.
Fixed
A set element, a map key and
CommitMergeResolution.chosenare handed out as copies: a caller could change them inside their container. Ships runtime 1.2.22.
1.2.6 — 2026-07-19¶
Ships runtime 1.2.21.
Changed
A
ValueStringmust be valid UTF-8, and documentation cannot contain"""; both were accepted before.
Added
ValueVector.concatandValueMap.mergereturn a new container and leave their operands untouched;extendandupdatestill work in place.index.d.tsdeclaresValueSet.containsandValueXArray.contains, which the binding had.
Fixed
The HTML renderer escapes names, strings and documentation. Ships runtime 1.2.21.
1.2.5 — 2026-07-13¶
Ships runtime 1.2.20.
Added
Value.collectCommitIds(value, type, definitions)andType.useCommitId(type), the commit-id twins ofcollectBlobIdsanduseBlobId.ValueXArray.items(encoded?), as in Python:items(false)yields the typed elements.ValueXArray.rebuildFrom(source, ...)copies a source xarray’s positions and tombstones and installs new elements in one step.
Fixed
A
ValueVarianthandle placed in a structure field keeps its value; it was re-read as an arm and reset (x: 9became0).extendDefinitionswith a type redefined under a newruntimeIdis refused before any write; it left a database that no longer opened. Ships runtime 1.2.20.
1.2.4 — 2026-07-06¶
Ships runtime 1.2.19.
Added
Value.toXmlString(value, indent?),Value.fromXmlString(string, type, definitions),DSMDefinitions.toXmlString(indent?)andDSMDefinitions.fromXmlString(string). Ships runtime 1.2.19.
1.2.3 — 2026-07-02¶
Ships runtime 1.2.18.
Removed (breaking)
jsonEncode,jsonDecode,bsonEncodeandbsonDecode, renamedtoJsonString,fromJsonString,toBsonBlobandfromBsonBlob, onValueandDSMDefinitions.ValueVec.toTuple()andValueMat.toTuple(): usetoArray().ValueOpcode.encode,decode,readandwrite.Logging.create(object), which only ever threw. Ships runtime 1.2.18.
Added
.at(-1)and.set(-1, …)on the indexed sequences:ValueVector,ValueVec,ValueTuple,ValueSet,BlobView,BlobArray, andValueMatper axis. Out of range still throwsRangeError.ValueVecis iterable, andValueVec/ValueMathavetoArray().A blob converts to and from a TypedArray:
BlobView.toTypedArray(),BlobArray.toTypedArray(),BlobArray.fromTypedArray(layout, ta), andBlobLayout.glAttribParams()forgl.vertexAttribPointer.toJSON()on every value: a 64-bit integer becomes a number when it fits exactly, else throwsRangeError; a blob becomes base64.Value.toBsonBlob/fromBsonBlob,DefinitionsConst.toDsmDefinitions,DefinitionsConst.write,Definitions.read,Definitions.extendConcepts,Path.readandPathConst.writework; they threw “not yet usable”.getIn/setIndescend through an optional, an any or a variant.Symbol.toStringTagon every class, andPathis iterable.
Fixed
An argument error crosses as a real
TypeErrororRangeError, not a genericErrorprefixed"exception ".index.d.tsdeclares the constructors as they are:CommitDatabasehas none public;TypeAny,TypeAnyConcept,BlobPackDescriptorandStreamWriterBlobhave one.ServiceRemoteFunction.callreturns its result.Value.fromJsonStringreads an integer literal into afloatordoublefield.
1.2.2 — 2026-06-30¶
Ships runtime 1.2.17.
Added
value.hashKey(), abigintfolding the type into the value’s hash, to key a JSMaporSetby value;hash()alone hashesInt8(1)andInt64(1)alike.[Symbol.dispose]on the resource handles, sousingreleases a database, a store or a stream at scope exit.Value.deducedescends into nested natives: an array, aSet, aMapor an object becomes the matching container.
Fixed
.compare()and.equals()accept any value and never throw: two values compare in the runtime’s total order. An any or a variant compares the operand as its own type. Ships runtime 1.2.17.
1.2.1 — 2026-06-29¶
Ships runtime 1.2.17.
Added
ServiceRemote.functionPoolFunc(pool, name)andattachmentFunctionPoolFunc(pool, name)reach one remote function by name.A prebuilt binary for macOS Intel.
Fixed
Definitions.decodeandDefinitionsConst.encodetake astreamCodecInstancingand default toStreamTokenBinaryCodec, as Python does; a definitions blob encoded by default did not decode. Ships runtime 1.2.17.
1.2.0 — 2026-06-27¶
First release — the in-process Node.js binding over the Viper runtime, bound roughly 1:1 with the Python surface via N-API.
Added — the Type/Value system, Commit Database, Definitions & DSM, Key/Path/Attachment, Blob, codecs, and the blocking remote-service client, with a structured exception bridge.
Added — native JS idioms:
number/bigintnumerics, prototype inheritance (instanceof),util.inspectrendering, object-literal structs (assign/toObject), Immutable.js-stylegetIn/setIn, strict non-negative.at().Added — two-axis versioning:
version()reports the binding,viperVersion()the embedded runtime.Added — prebuilt binaries for Linux (x64/arm64), macOS (Apple Silicon), and Windows (x64/arm64); N-API ABI-stable, no toolchain required at install.
Note — the remote-service server tier, transport, and IPC/threading primitives are intentionally not bound (incompatible with Node’s event loop).
Ships runtime 1.2.