Skip to content

Releases

v2.28.0

Choose a tag to compare

@github-actions github-actions released this 08 Nov 15:02

Two algorithms for a plane whose edges mean depends on, from two issues filed
against this repository. A graph database that can rank, cluster and connect
nodes but cannot say what order to process them in leaves its caller to walk
the edges by hand — and the hand-written version is the one that returns a
partial answer looking like a whole one when the graph turns out to have a
loop.

Added

  • toposort: dependency order, with a generation per node. plane.algo,
    drsg algo toposort and the MCP algo tool answer [{id, order, generation}], where generation is the node's longest-path depth — so two
    nodes of one generation have no path between them and can be processed in
    parallel. That is the whole point of reporting it, and it is why the order is
    peeled in waves rather than drained from a single queue: the naive version
    numbers a node by whichever predecessor released it, which is not its depth.

    An edge_type names the one relation that claims the order. It matters more
    than it looks: an order is a statement about a single relation, and a plane
    holding several kinds of edge can be a DAG under the one you mean while some
    unrelated edge closes a loop across it. Left unset, every type counts, which
    is the old whole-plane behaviour and rarely what a mixed plane wants. dir
    reads edges the other way, for a plane whose edges point dependent to
    dependency.

    A cyclic scope is refused, naming a node the cycle actually contains — not
    the partial order Kahn's algorithm leaves behind, which has every appearance
    of a complete answer. The named node is reached by walking predecessors
    through what the algorithm could not place, so it lies on the cycle rather
    than merely downstream of one.

  • cycles: the loop itself, not a boolean. Each result is the node list
    that closes one cycle — [12, 7, 9, 12] — so a write that must not create a
    loop can refuse while naming the nodes at fault. No result means the scope is
    a DAG, which is the other reason to ask; components cannot answer that, a
    DAG and a cyclic graph being alike one weakly connected component. limit
    caps the search rather than the output, so limit: 1 stops at the first
    cycle. One depth-first pass reporting one cycle per back edge: enough to
    prove a graph cyclic and name the fault, not an enumeration of every cycle,
    which is exponential in the worst case. No direction option — reversing every
    edge maps a cycle onto its own reverse.

Neither is a Cypher CALL. That surface reports one scalar per node through
score(); an order gives each node two, and a cycle is a path rather than a
node at all. The five SDKs that bind plane.algo are regenerated, the
TypeScript union of algorithm names among them.

The pair ships with property tests over random small graphs — duplicate edges
and self-loops included, since parallel edges are what in-degree counting gets
wrong — checked against a naive model written beside them rather than against
each other, which one bug in the shared frame would satisfy twice over.

Full Changelog: v2.27.1...v2.28.0

v2.27.1

Choose a tag to compare

@github-actions github-actions released this 08 Nov 06:44

Packaging only: the npm wrapper could not prove it belonged to this project,
which is the one thing standing between drsg and a listing in the official MCP
registry. Nothing in the engine, the CLI or the SDKs changed.

Fixed

  • The npm wrapper declares its MCP server name. The registry verifies that
    a package belongs to the server it is listed under by reading mcpName from
    the published package.json and matching it against the record's name.
    @yingge/drsg was published without the field, so a publish of the record
    was refused however the namespace login went — and nothing on the npm side
    said anything was missing, because npm does not care about the field. It is
    derived from the repository rather than written out, so the two cannot
    drift apart.

Changed

  • The registry record is the npm package, at the current version.
    packaging/mcp-registry/server.json listed the OCI image at 2.21.0, a
    compromise from when the only other option was a platform tarball, which is
    not a package type the registry fetches. It now names @yingge/drsg, runs
    serve on a stated address rather than a defaulted one, and marks
    DRSG_TOKEN required — /mcp is gated at write level and a tokenless
    server trusts only its own browser UI, so a client following the old record
    would have connected and then been refused every call with nothing
    explaining why. just bump-version now moves this file with the rest.

What's Changed

  • chore(deps): bump the cargo group across 1 directory with 2 updates by @dependabot[bot] in #97
  • chore(deps): bump tokio-tungstenite from 0.29.0 to 0.30.0 by @dependabot[bot] in #95
  • chore(deps): bump the actions group with 6 updates by @dependabot[bot] in #96

Full Changelog: v2.27.0...v2.27.1

v2.27.0

Choose a tag to compare

@github-actions github-actions released this 07 Nov 17:03

Two halves of one problem, found while building a plugin that reads the SQL a
repository writes. A parser can mint keys only for what it parses, and an edge
onto a key no node declares fails the entire write — so when two parsers
describe the same lines, neither can say so, and the graph was quietly the
poorer for it in both directions at once.

Added

  • context joins what no edge relates. A node that records file, line
    and end_line now also lists the nodes whose span holds it and the nodes its
    span holds, under inside (by span) and holds (by span). The occasion is an
    SQL statement written in a Rust string literal: the statement and the function
    around it are facts from two different parsers, each unable to name the
    other's key, and the lines they both record are enough to tell a reader which
    function issues which query. Reported as position and labelled as such, never
    as an edge, because a reader who cannot tell the two apart is reading a claim
    the graph never made.

    It stays silent unless it has something to say. A pair that any edge already
    relates is left to that edge, so the sections are empty for code one parser
    read on its own — a module and its functions, a table and its columns — and
    speak only where two parsers met. Equal extents are not containment in either
    direction: two parsers describing the same lines is not one holding the other.

Fixed

  • A declaration outranks a stand-in in either order. When two plugins claim
    one key, the one that parsed the real thing wins and the stand-in's extra
    labels are absorbed into it. That was already the intent, but the rule fired
    only when the stand-in arrived second. Handler order is install order, read
    from the plugin store, which no plugin author can see or rely on — so the
    outcome depended on the order two plugins happened to be installed in.

    What it cost: a Java repository with MyBatis mappers and SQL migrations lost
    the real Table and every Column on it — their types, their nullability,
    their primary key — in favour of the empty stand-ins a mapper writes for the
    tables it queries, and said so only as four collision notes. The same
    asymmetry would have destroyed java's own Method declarations had xml
    been installed ahead of it. Node and edge counts were unchanged throughout,
    which is why nothing looked wrong.

Full Changelog: v2.26.0...v2.27.0

v2.26.0

Choose a tag to compare

@github-actions github-actions released this 07 Nov 05:07

Another repository reported what driving the MCP server felt like from the
outside, and most of this release is that report. Four of its findings
reproduced here, two of them worse than described, and they share a shape: an
answer that was true and read as something else. A radius of zero that looked
like "nothing calls this". A candidate list whose entries could not be picked.
A re-export path that resolved to an empty node instead of missing honestly. A
grep that searched generated output as readily as source.

Pairs with the rust parser plugin's own release, which closes the two halves
of this a parser owns.

Added

  • A zero radius says whether it is an absence or a boundary. impact now
    prints, beside the total and never inside it, the MAY_CALL candidates a
    parse recorded and the count of unresolved references that merely share the
    symbol's name — and when the total is zero and either is not, it says so
    outright. "Nothing reaches this" and "the parse could not tell" were the same
    0, with the same honesty note under it, and nothing in the answer let a
    reader tell them apart. That is how a symbol whose caller a grep finds
    immediately came to read as dead.
  • MAY_CALL, an edge for a call a parse cannot prove. Deliberately absent
    from the radius: what would break if a symbol changed is what impact
    counts, and a candidate is not that — counted in the total it would turn a
    documented lower bound into a guess. It is queryable like any other edge
    type, so cypher and traverse reach it, and the basis of each one rides on
    the edge rather than being inferred from its existence.
  • A miss on a re-export path names the declaration. pub use capability::Grant makes ip_core::Grant a path the source writes and the
    compiler accepts, while the graph keys a symbol by where it is declared — so
    the lookup missed and the answer recited the prefixes the plane holds, which
    answers a question nobody asked. It now names the key instead, and suppresses
    the prefix listing when it has. A lookup that lands on a definition-less
    stand-in says the same thing, and adds that counts taken from there are about
    the stand-in and not the declaration.
  • grep takes all_files (--all-files on the CLI) for the cases where
    build output is the thing being searched.

Changed

  • grep honours the tree's ignore files by default, and walks it with the
    same ignore crate the digest uses rather than a second hand-rolled walker.
    The graph is built from the files those rules admit, so the text search now
    covers the same set: a hit in a generated bundle is not a hit in the source
    it was built from, and it used to bury it. Two visible consequences, which
    is why this is a minor and not a patch — the default result set is smaller,
    and hits now arrive in lexicographic depth-first order, where the previous
    order ran sibling directories backwards as an artifact of the walk. Which
    hits survive max_results depends on that order, so it is now a pinned
    contract rather than an accident.
  • The shell guard's reason vocabulary: =outside replaces =notcode. The
    retired word claimed docs, config and markdown for the shell, and the plane
    holds every text file in the tree — grep and snippet answer a markdown or
    TOML question as readily as a Rust one. So the word routed agents to the
    shell for precisely the searches drsg answers. DRSG_RAW=notcode is now
    corrected where it is typed rather than counted as a reasonless hatch, since
    the alternative is twenty-four silent passes before the nag mentions the new
    name.

Fixed

  • An unresolved reference is no longer offered as an ambiguity candidate.
    A plugin that cannot bind a call records the far end as a stub, one per file
    that makes the call, and those stubs were competing with the definitions:
    context with_context returned fifteen candidates of which eleven were the
    same unbound name, once per file, none of them a key that resolves to
    anything readable. Only declarations decide whether a name is ambiguous now —
    so one declaration among a dozen stubs simply resolves — and whatever stubs
    remain are counted in one line rather than listed. A name that matches
    only stubs gets its own answer, naming the call sites and the next call to
    make, instead of the bare "no symbol matches" that would deny the name is
    used here at all.

Full Changelog: v2.25.0...v2.26.0

v2.25.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 19:34

A batch that overlaps what a plane already holds can now be ingested instead
of refused. The query language has had MERGE for a long time; the bulk path
had not, so a re-ingest was the caller's problem and only drsg import had
solved it.

Added

  • The bulk path takes a conflict policy, through WriteTxn::bulk_upsert
    and OnConflict. Error refuses the batch and stays the default, so every
    existing caller is unchanged; Skip keeps the existing node and drops the
    incoming one; Update writes the incoming properties over it and replaces
    its labels when the incoming node carries any — properties not mentioned
    are left alone, the soft schema's rule that absence is not a deletion.

    A kept node keeps its id, which is the point rather than a detail:
    delete_node cascades to incident edges, so a policy that replaced the
    node would have silently emptied the graph around every key it touched. Ids
    are still reserved for every input line in one go, and the one belonging to
    a line that turned out to collide is simply never used.

    Reachable from drsg import --on-conflict (where it already was), the MCP
    write_nodes tool, digest.write over JSON-RPC, and the five generated
    SDKs. A repeat within one batch stays an error whatever the policy says:
    two lines claiming one key have no existing node to defer to.

  • digest.write reports what it updated — nodes_updated and
    updated_keys beside the skip counts. Both are empty unless the policy
    asked for updates, so a client that never sets one sees the reply it saw
    before. Calling an updated node "skipped" is the kind of half-true report
    that let a silent skip go unnoticed for weeks once already.

  • A benchmark for what a re-ingest costs — upsert-fresh, upsert-skip
    and upsert-update over one batch, so the three prices are comparable.
    Updating runs about 4× slower than a fresh insert: the merge goes through
    the property path, which is where index membership and the change feed are
    maintained and therefore cannot be batched the way the loader's writes are.

Changed

  • The MCP write_nodes tool writes through the bulk path. It looped
    create_node_with_key before, one record at a time. The reply is unchanged
    for a caller who sets no policy.
  • digest.write defaults to skip, not error, unlike every other write
    surface. It is not new behaviour — it is what the method already did, now
    said out loud as a parameter. A distillation proposes every entity as new,
    so a key the plane already owns is the normal case there, and refusing the
    batch would drop the run exactly when it had learned something.
  • The package managers are on the first screen. npm, crates.io and PyPI
    version badges in the README header, and a line in "Three commands" naming
    npx @yingge/drsg and cargo install drsg. All three channels opened in
    2.24.4–2.24.5 and the README mentioned them only 170 lines down.
  • Every module has a maintainer named in CONTRIBUTING.md, and
    dr-strange-net joins the module table it had been missing from.

Fixed

  • The change feed and the index mirroring no longer assume a dense id
    range.
    Both walked node_start + i over a bulk load's reservation, which
    is only correct while nothing collides. They read the loader's positional
    resolution now — a latent wrong only reachable once the reservation could
    have holes in it, which is to say, once this release's policies existed.

Full Changelog: v2.24.5...v2.25.0

v2.24.5

Choose a tag to compare

@github-actions github-actions released this 03 Nov 13:26

Finishes what 2.24.4 started. Two of the eight crates could not be published,
for a reason that only appears at the registry's door, and the check that
would have caught it now runs on every push.

Fixed

  • The embedded web assets are staged in OUT_DIR. dr-strange-web's
    build script backfilled a placeholder frontend/dist/index.html so a build
    works without the JS toolchain — writing into the source tree, which
    cargo publish rejects outright: it builds the extracted package and then
    diffs the directory. The bundle is now copied into OUT_DIR and
    rust-embed reads it from there, so nothing outside it is touched. The
    placeholder behaviour is unchanged, and a real just web-build bundle still
    wins when one exists.
  • dr-strange-web and drsg reach crates.io. They are the two 2.24.4
    could not publish; the other six went up before the failure and stay at
    2.24.4 there. All eight are at 2.24.5, so cargo install drsg works from
    this release on.

Added

  • The gate and CI refuse a build script that writes into the source tree.
    It is invisible to every local build and to cargo package --no-verify,
    and surfaces only during a publish — by which point the crates ahead of it
    in the release are already on a registry that does not take them back. The
    check builds where no build output exists and fails on any file that
    appears.

Full Changelog: v2.24.4...v2.24.5

v2.24.4

Choose a tag to compare

@github-actions github-actions released this 03 Nov 11:15

A distribution release. No engine change: drsg reaches two package managers
it was not on before, and the npm wrapper takes the name npm will actually
give it.

Added

  • The crates are published to crates.io. cargo install drsg installs the
    binary, and the seven libraries behind it — core, log, net, parser, llm,
    mcp, web — are on the registry so they can be depended on directly. The
    release job publishes them in dependency order, skips a version already
    there so a re-run is safe, and skips itself entirely where no token is
    configured, the way the npm and PyPI jobs already did. The benchmark
    harness and the PyO3 crate are marked publish = false: one is a
    development tool, the other ships to PyPI as a wheel no Rust crate can
    link against.
  • drsg-embedded is on PyPI, five cp39-abi3 wheels covering Linux
    x86_64 and aarch64, macOS x86_64 and arm64, and Windows x86_64. The
    in-process binding is alpha and says so; cypher, the change feed and bulk
    writes are the next additions.

Changed

  • The CLI crate is named drsg. It was dr-strange-cli, which would have
    made the install command cargo install dr-strange-cli for a binary called
    drsg. The directory keeps its name, so crates/dr-strange-cli is
    unchanged and only the cargo selector moves: -p dr-strange-cli is now
    -p drsg.
  • The npm wrapper is @yingge/drsg. The bare name cannot be had — npm
    refuses it as too similar to the existing package arg, which is its
    typosquatting filter and not something a publish can argue with. The five
    platform packages keep their drsg-* names, and the installed command is
    still drsg, so this costs the install string and nothing else.
  • Every intra-workspace dependency carries a version beside its path.
    cargo publish strips the path and resolves against the registry, and
    refuses a manifest that offers only a path. They are declared in one place,
    and just bump-version now rewrites them and fails if one is left behind —
    a local build resolves through the path and never reads these, so a stale
    version is invisible until it ships a crate depending on the last release.

Full Changelog: v2.24.3...v2.24.4

v2.24.3

Choose a tag to compare

@github-actions github-actions released this 02 Nov 16:30

A dependency release. The plugin sandbox takes four upstream security patches,
and one of the clients under the proxy policy changed how it reads a
password — the only thing here an operator may notice.

Security

  • The plugin sandbox takes wasmtime 48.0.8. RUSTSEC-2026-0420, -0422,
    -0423 and -0424: a wasip3 UDP send able to reach an address only allowed
    for receiving, a panic on typed functions with i31 or nullref arguments,
    cross-engine type confusion through host-created struct types with
    supertypes, and unchecked memory use when borrowed resources move between
    components. All four were published about four hours after 2.24.2's own CI
    had run green, so the release that shipped them was green when it was cut.
    That sandbox is the whole of the plugin security story — a plugin reaches
    nothing but three host functions, rooted at the digested directory — so an
    advisory with a fix is taken, not reasoned about. A patch bump inside the
    pinned major; the guards it exists for (no filesystem, no sockets, fuel and
    deadline) are unchanged and still asserted.
  • A fourth zip advisory is recorded as unreachable. RUSTSEC-2026-0429
    joins the family deny.toml already documents: a symlink entry whose
    declared uncompressed size sizes the target buffer, aborting the process
    rather than panicking. Like its three siblings it lives inside
    ZipArchive::extract(), and nothing here extracts — a .docx or .xlsx is
    read in memory through by_name. The fix is zip >= 9.0.0 and every
    published anydoc requires ^8.6.0, so there is still nothing to upgrade to.

Changed

  • A % in a proxy password now means what the URL says it means. ureq
    3.5.0 decodes proxy credentials exactly once, where 3.4.2 put the raw
    substring of the URL on the wire. An operator whose proxy password contains
    a percent sign was authenticating with the encoded text and must now write a
    literal one as %25. This closes a divergence rather than opening one: the
    MCP relay dials the same configured value through reqwest, which has always
    decoded it, so one password could authenticate two different ways depending
    on which client made the request. A credential that does not decode to UTF-8
    is now refused where it is written instead of at the first request. The
    bypass list is untouched: NO_PROXY is decided here, before a proxy is
    built, and the proxy handed to a client carries no list of its own.
  • One WebSocket stack instead of two. tokio-tungstenite 0.24 → 0.29 meets
    the 0.29 already in the tree, so the duplicate pair is gone. scraper
    0.27 → 0.29, rmcp 3.6 → 3.8, reqwest 0.13.5, ego-tree 0.11.1 and
    ignore 0.4.34 come with it, and the socks crate leaves the tree — ureq
    3.5.0 dropped the dependency.

What's Changed

  • chore(deps): bump the actions group with 6 updates by @dependabot[bot] in #90
  • chore(deps-dev): bump org.apache.maven.plugins:maven-surefire-plugin from 3.6.0 to 3.6.1 in /sdk/java by @dependabot[bot] in #86
  • chore(deps): bump tokio-tungstenite from 0.24.0 to 0.29.0 by @dependabot[bot] in #88
  • chore(deps): bump scraper from 0.27.0 to 0.29.0 by @dependabot[bot] in #89
  • chore(deps): bump the cargo group across 1 directory with 5 updates by @dependabot[bot] in #91
  • chore(deps): the cargo group, less the ureq pin by @wangyingsm in #92
  • test(net): the proxy guard the ureq bump called for by @wangyingsm in #93

Full Changelog: v2.24.2...v2.24.3

v2.24.2

Choose a tag to compare

@github-actions github-actions released this 30 Oct 07:31

Three fixes around what a plane knows about itself. A rebuild replaces a
plane's facts, and it was taking the plane's own record with them; the ops
plane beside it had no record to take.

Fixed

  • A rebuild keeps what the plane itself records. --force — and so
    drsg init --rebuild — dropped the plane and recreated it carrying only the
    marker that says a rebuild is in progress, which took every property with
    it. An attachment is one of those: plane.attach records it on the plane,
    not in a side table, so re-reading a tree silently detached every graph that
    plane could reach, and anything plane.set_props had written went the same
    way. The properties are now read before the drop and carried into the new
    plane; the provenance and sync-point ones the rebuild owns are rewritten
    before it clears the marker, so nothing reads a stale one from a plane that
    claims to be finished. Mounted databases were never affected — they are
    recorded on the startup plane and replayed on each start.
  • The ops plane records the plugin set that built it. <plane>_ops carried
    no provenance at all, so the drift check (#39) could not say whether a
    workload or image predated a change to the Compose, Kubernetes or Terraform
    reader that produced it — while the code plane beside it said exactly that
    about the code. It is stamped on a full rebuild, where the code plane's stamp
    is written and for the same reason: a stamp that moved on every fold would
    claim the whole plane had been reparsed when one commit's files had.
  • The ops plane records its sync point. The commit, the working-tree drift
    and the directory — the same three the code plane beside it has long
    recorded. A reader of <plane>_ops could see the workloads but not how old
    they were. Unlike the plugin stamp this moves on every fold, because every
    fold does re-read the manifests.

Full Changelog: v2.24.1...v2.24.2

v2.24.1

Choose a tag to compare

@github-actions github-actions released this 30 Oct 01:04

One bug, reported from outside (#84): a proxy an operator set for the outside
world was swallowing the local server a repository declares for itself. The
second fix here was found while testing the first.

Fixed

  • A proxy in the environment no longer swallows the declared server (#84).
    reqwest selects a system proxy on its own and makes no exception for
    loopback, so the liveness probe for the drsg serve … watch a repository
    names in its .mcp.json — on 127.0.0.1 — went to the operator's
    http_proxy, and so did the relay's own session, carrying the
    Authorization that file holds to a proxy that had no business seeing it.
    With no database in the directory the failure was loud: no database at 'graph.drsg'. With one present it was silent — initialize succeeded,
    tools/list answered, the process exited 0, and the host read the local
    on-disk graph instead of the live plane with nothing on stdout to say so,
    which is the worse half of this bug. Both of drsg-mcp's HTTP clients now
    decide per destination: loopback and NO_PROXY hosts go direct, and a
    declared server that is genuinely elsewhere still goes through the proxy.
  • The fallback says why it fell back. A dead server, a proxy refusing on
    its behalf and a URL with no origin to probe were one false between them,
    logged at debug, which is why every one of them read as a dead server. The
    reason is now named with its cause chain — Connection refused, a timeout,
    a status — and recorded at info, because that line marks the graph being
    served changing out from under the host.
  • A host that hangs up before initialize no longer wedges the relay.
    drsg-mcp < /dev/null, or a host that dies between launch and its first
    message, left the process running until it was killed. The relay's server
    transport is now closed only if a message ever went through it: rmcp 3.6.0's
    worker waits for its first one in a bare channel receive with no branch for
    the cancellation its own close sends, so closing an unused transport never
    returns. The 2.21.0 binary behaves the same way, so this is not new.

Changed

  • The proxy policy is its own crate, dr-strange-net. It sat in
    dr-strange-llm because that was once the lowest crate making an outbound
    request, which stopped being true; the MCP relay needs the same decision and
    had no business reaching through the LLM crate for it. The policy itself is
    unchanged — the loopback exception, the NO_PROXY grammar and the
    precedence of ALL_PROXY over the per-scheme variables are the same code,
    with the same tests, now shared by the LLM client, the web fetcher and the
    relay. dr_strange_llm::net is gone as a public path; dr_strange_net
    replaces it. These crates are not published to crates.io, so nothing outside
    this workspace depended on the old one.

What's Changed

  • fix(mcp): the probe and the relay never proxy loopback by @wangyingsm in #85

Full Changelog: v2.24.0...v2.24.1