• v0.13.0 047932f508

    v0.13.0 Stable

    x released this 2026-10-22 14:11:52 +00:00 | 3 commits to main since this release

    VSKI v0.13.0

    Light graphs: calendar-grade indexing

    Light graphs landed in v0.10 as a relationship index for collections, and
    the first heavy users all converged on the same ask: index events by
    time. "What did this person do last week", "what changed on this date",
    "how many hours accumulated on this edge" - the graph data model already
    fit (nodes and edges carry labels, weights and free-form JSON properties),
    but the ingest and query primitives were missing. v0.13.0 closes that gap
    without growing the engine: no new tables, no migrations, one new index.

    Idempotent ingest: upsert=true

    Indexing pipelines re-run. POST /graphs/:name/nodes|edges?upsert=true
    (now also graph.createNodes(data, { upsert: true }) in the JS SDK)
    replaces an existing id wholesale instead of erroring - the graph
    equivalent of MERGE, and the primitive every timetree ingester needs.
    Upserts hitting an existing id additionally check the graph's updateRule
    (the item already passed createRule like any create). The whole batch is
    written in a single transaction: on failure nothing is persisted (batch
    creates previously could persist a prefix of the batch).

    Atomic accumulation: weightDelta

    PATCH /graphs/:name/edges/:id accepts weightDelta (mutually exclusive
    with weight): the server reads the current weight under the graph's
    write lock and adds to it, so concurrent increments cannot lose updates -
    the classic "hours accumulated", "stock consumed" counter. Weights also
    feed a new stats aggregate: GET .../stats now reports
    edgeWeightSums per edge label (computed in SQL, where NULL weights are
    distinguishable from 0) alongside a new nodeLabels histogram.

    Property filters: prop=path op value

    The JSON properties maps are no longer opaque. List endpoints accept
    repeatable prop filters - ref.collection=users, capacity>=5,
    status!=draft, title~meeting, archived=null - compiled to SQLite
    json_extract comparisons with bound parameters (numeric-looking values
    compare numerically, ~ is a substring match, null tests for
    absence). This makes properties.ref and capacity/stock metadata
    first-class query targets. Nodes also accept ids=a,b,c (max 500) and
    edge source/target accept comma lists, so "everything touching last
    week's day nodes" is one request.

    Node types in the index: nodeLabels

    Node labels are now interned in the in-memory topology index (alongside
    edge labels and weights; ~4 bytes/node). neighbors, traverse and
    subgraph accept nodeLabels=a,b to restrict visited/returned nodes by
    type - "documents linked to this day, not the people". Node label PATCHes
    keep the index in sync; a SQL index on nodes(label) is created for new
    graphs and backfilled idempotently for existing ones.

    Correctness fixes

    • Edge weight PATCHes left the in-memory index stale on the master:
      the index entry was rebuilt only when the label changed, and with the
      pre-update weight - weighted shortest paths kept answering with the old
      cost until restart. Weight and label changes now sync the index
      immediately.
    • Shortest path ignored edge direction - it always relaxed both
      adjacency lists, so directed graphs got undirected routes. The default
      stays both-ways (documented behavior), but path now accepts
      direction=out|in|both.

    Calendar timetree, packaged

    The Calendar Timetree recipe
    documents the pattern end to end: week --includes_day--> day backbone
    nodes, record --on_day--> day activity edges with magnitudes on the
    weight, deterministic ids so re-ingest is a no-op. The new
    calendar-index example plugin automates it - subscribe it to a
    collection with a date field and it maintains the tree on every
    create/update/delete (record deletes cascade their edges; the calendar
    backbone stays), with backfill and week_of helper functions. It uses
    native upsert (hence vski-sdk >= 0.6.0) and needs read rules on the
    source collection plus write rules on the target graph for its service
    account (the recipe shows both).

    The server stays calendar-agnostic: any bucketing (sprints, fiscal
    quarters) is just more backbone nodes with the same shape.

    SDKs

    • JS/TS: createNodes/createEdges take an optional
      { upsert } second argument; updateEdge takes weightDelta;
      path takes direction; getNodes takes ids and prop,
      getEdges takes array source/target and prop;
      neighbors/traverse/subgraph take nodeLabels; GraphStats adds
      nodeLabels and edgeWeightSums. All additive.
    • Python (vski-sdk v0.6.0): create_nodes/create_edges take
      upsert=; update_edge takes weight_delta=; get_nodes takes ids=
      and prop=, get_edges takes list-valued source=/target= and
      prop=; neighbors/traverse/subgraph take node_labels=; path
      takes direction=; GraphStats adds nodeLabels and edgeWeightSums.
      The sync wrapper mirrors everything automatically.

    Tests

    47_graph_test.ts grows a "Calendar indexing primitives" step (upsert
    replacement + duplicate rejection, weightDelta accumulation and
    exclusivity, weight-patch freshness on weighted paths, directed path,
    property/ids/multi-value filters, nodeLabels on neighbors and subgraph,
    stats sums); wiki_graph_verify.ts pins every new claim the wiki pages
    make; graphdb unit tests cover SetEdgeWeight, SetNodeLabel,
    directed paths and node-label filters. On the Python side, the SDK's
    test_47_graphs.py mirrors the new surface and test_53_calendar_index.py
    exercises the calendar-index plugin end to end (tree build, re-ingest
    idempotency, delete cascade, week_of, backfill). A pre-existing flaky
    SchemaFromMap test (map iteration order) is now order-independent.

    Compatibility

    • No breaking API or storage changes; everything new is opt-in query/body
      parameters. Path defaults to the historical both-ways walk; plain
      POST stays insert-only.
    • No migrations: the node-label index is CREATE INDEX IF NOT EXISTS,
      applied on graph creation and backfilled lazily (master only) when an
      existing graph's index loads.
    • Upsert replaces label/properties (and edge endpoints/weight)
      wholesale - it does not merge. Pipelines that relied on the
      create-then-update fallback in graph-sync keep working unchanged.
    Downloads