Repository navigation
Releases
v2.28.0
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 toposortand the MCPalgotool answer[{id, order, generation}], wheregenerationis 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_typenames 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;componentscannot answer that, a
DAG and a cyclic graph being alike one weakly connected component.limit
caps the search rather than the output, solimit: 1stops 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
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 readingmcpNamefrom
the publishedpackage.jsonand matching it against the record'sname.
@yingge/drsgwas 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.jsonlisted 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
serveon a stated address rather than a defaulted one, and marks
DRSG_TOKENrequired —/mcpis 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-versionnow 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
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
-
contextjoins what no edge relates. A node that recordsfile,line
andend_linenow also lists the nodes whose span holds it and the nodes its
span holds, underinside (by span)andholds (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 realTableand everyColumnon 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 destroyedjava's ownMethoddeclarations hadxml
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
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.
impactnow
prints, beside the total and never inside it, theMAY_CALLcandidates 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 agrepfinds
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 whatimpact
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, socypherandtraversereach 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::Grantmakesip_core::Granta 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. greptakesall_files(--all-fileson the CLI) for the cases where
build output is the thing being searched.
Changed
grephonours the tree's ignore files by default, and walks it with the
sameignorecrate 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 survivemax_resultsdepends on that order, so it is now a pinned
contract rather than an accident.- The shell guard's reason vocabulary:
=outsidereplaces=notcode. The
retired word claimed docs, config and markdown for the shell, and the plane
holds every text file in the tree —grepandsnippetanswer 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=notcodeis 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_contextreturned 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
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
andOnConflict.Errorrefuses the batch and stays the default, so every
existing caller is unchanged;Skipkeeps the existing node and drops the
incoming one;Updatewrites 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_nodecascades 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_nodestool,digest.writeover 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.writereports what it updated —nodes_updatedand
updated_keysbeside 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
andupsert-updateover 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_nodestool writes through the bulk path. It looped
create_node_with_keybefore, one record at a time. The reply is unchanged
for a caller who sets no policy. digest.writedefaults toskip, noterror, 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/drsgandcargo 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-netjoins the module table it had been missing from.
Fixed
- The change feed and the index mirroring no longer assume a dense id
range. Both walkednode_start + iover 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
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 placeholderfrontend/dist/index.htmlso a build
works without the JS toolchain — writing into the source tree, which
cargo publishrejects outright: it builds the extracted package and then
diffs the directory. The bundle is now copied intoOUT_DIRand
rust-embedreads it from there, so nothing outside it is touched. The
placeholder behaviour is unchanged, and a realjust web-buildbundle still
wins when one exists. dr-strange-webanddrsgreach 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, socargo install drsgworks 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 tocargo 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
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 drsginstalls 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 markedpublish = false: one is a
development tool, the other ships to PyPI as a wheel no Rust crate can
link against. drsg-embeddedis on PyPI, fivecp39-abi3wheels 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 wasdr-strange-cli, which would have
made the install commandcargo install dr-strange-clifor a binary called
drsg. The directory keeps its name, socrates/dr-strange-cliis
unchanged and only the cargo selector moves:-p dr-strange-cliis now
-p drsg. - The npm wrapper is
@yingge/drsg. The bare name cannot be had — npm
refuses it as too similar to the existing packagearg, which is its
typosquatting filter and not something a publish can argue with. The five
platform packages keep theirdrsg-*names, and the installed command is
stilldrsg, so this costs the install string and nothing else. - Every intra-workspace dependency carries a version beside its path.
cargo publishstrips the path and resolves against the registry, and
refuses a manifest that offers only a path. They are declared in one place,
andjust bump-versionnow 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
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 UDPsendable to reach an address only allowed
for receiving, a panic on typed functions withi31ornullrefarguments,
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 familydeny.tomlalready 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.docxor.xlsxis
read in memory throughby_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_PROXYis 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,rmcp3.6 → 3.8,reqwest0.13.5,ego-tree0.11.1 and
ignore0.4.34 come with it, and thesockscrate 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
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.attachrecords it on the plane,
not in a side table, so re-reading a tree silently detached every graph that
plane could reach, and anythingplane.set_propshad 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 thestartupplane and replayed on each start. - The ops plane records the plugin set that built it.
<plane>_opscarried
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>_opscould 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
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 thedrsg serve … watcha repository
names in its.mcp.json— on127.0.0.1— went to the operator's
http_proxy, and so did the relay's own session, carrying the
Authorizationthat 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 —initializesucceeded,
tools/listanswered, 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 ofdrsg-mcp's HTTP clients now
decide per destination: loopback andNO_PROXYhosts 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 onefalsebetween 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
initializeno 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 ownclosesends, 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-llmbecause 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, theNO_PROXYgrammar and the
precedence ofALL_PROXYover 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::netis 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