#![cfg(all(feature = "mcp", feature = "context", feature = "persistence"))]
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use rmcp::service::RunningService;
use rmcp::{RoleClient, ServiceExt};
use tempfile::TempDir;
use velesdb_memory::mcp::McpServer;
use velesdb_memory::{DynEmbedder, HashEmbedder, MemoryService, DEFAULT_DIMENSION};
struct Binding {
name: &'static str,
path: &'static str,
surface_impl: &'static str,
}
const BINDINGS: &[Binding] = &[
Binding {
name: "velesdb-node",
path: "crates/velesdb-node/src/lib.rs",
surface_impl: "impl MemoryStore {",
},
Binding {
name: "velesdb-python",
path: "crates/velesdb-python/src/agent_memory_service.rs",
surface_impl: "impl PyMemoryService {",
},
Binding {
name: "velesdb-wasm",
path: "crates/velesdb-wasm/src/memory_service.rs",
surface_impl: "impl WasmMemoryService {",
},
];
struct Exemption {
binding: &'static str,
tool: &'static str,
reason: &'static str,
}
const DAEMON_MIGRATION_ONLY: &str = "online migration owns the native daemon's live generation, process lock, durable control directory and environment-backed target factory; an in-process language binding owns none of that control plane, so exposing this MCP operation there would create a second unsafe migration authority";
const EXEMPTIONS: &[Exemption] = &[
Exemption {
binding: "velesdb-node",
tool: "migration_start",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-node",
tool: "migration_status",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-node",
tool: "migration_cancel",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-node",
tool: "migration_recover",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-python",
tool: "migration_start",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-python",
tool: "migration_status",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-python",
tool: "migration_cancel",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-python",
tool: "migration_recover",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-wasm",
tool: "migration_start",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-wasm",
tool: "migration_status",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-wasm",
tool: "migration_cancel",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-wasm",
tool: "migration_recover",
reason: DAEMON_MIGRATION_ONLY,
},
Exemption {
binding: "velesdb-node",
tool: "extraction_status",
reason: "this status surface exists for the MCP transport's durable background receipt; \
the Node binding keeps MemoryService::remember_extracted synchronous and returns \
its final ids directly, so it has no background request_id to query",
},
Exemption {
binding: "velesdb-python",
tool: "extraction_status",
reason: "this status surface exists for the MCP transport's durable background receipt; \
the Python binding keeps MemoryService::remember_extracted synchronous and \
returns its final ids directly, so it has no background request_id to query",
},
Exemption {
binding: "velesdb-wasm",
tool: "extraction_status",
reason: "WASM rememberExtracted is synchronous and in-memory, while durable extraction \
receipts require the native persistence directory that wasm32 deliberately omits",
},
Exemption {
binding: "velesdb-wasm",
tool: "feedback",
reason: "a durable learned confidence is meaningless on the in-memory WASM backend: \
MemoryService::feedback lives in the `persistence`-gated `reinforce` module and \
is not compiled for wasm32 at all — exposing it would mean pulling \
NativeStore/filesystem code into the very bundle this binding exists to avoid",
},
Exemption {
binding: "velesdb-wasm",
tool: "list_memories",
reason: "the WASM backend keeps MemoryStore::list default refusal - its in-memory \
store lives and dies with the page, so what-does-my-agent-know is answered \
by the caller own state, not by an audit walk",
},
Exemption {
binding: "velesdb-wasm",
tool: "memory_status",
reason: "the WASM backend is in-memory and persistence-free: no provenance record, no \
store directory, no autograph worker — every block of the status except the \
fact count is structurally absent from that build",
},
];
struct ShapeDivergence {
binding: &'static str,
tool: &'static str,
field: &'static str,
reason: &'static str,
}
const ID_TWIN: &str = "deliberate unwrap of the id twin: the MCP wire carries an id BOTH as a \
JSON number and as its decimal-string copy, because a u64 above 2^53 is lossy on a \
float-lossy JSON client. A typed binding return has no such problem — it hands back one \
form (a decimal string on JS, a native int on Python) and the twin has nothing to add";
const SINGLE_MEMBER: &str = "deliberate unwrap: the envelope carries exactly one useful member \
and exists only because the MCP spec requires an object at the output-schema root — a \
constraint of the transport, not of the domain. Nothing is lost";
const FORGET_BOOL: &str = "deliberate unwrap: the binding returns the bare boolean, which IS \
this envelope's `found` — whether a memory existed under that id and was deleted. The \
echoed id adds nothing to a call the caller made with that id in hand";
const DATED_SPLIT: &str = "deliberate split, not a drop: the dated half of fused recall is a \
SECOND binding method (`recallFusedDated`), which returns the timeline and the clock. \
This method is the undated one, and returns the bare memories array";
const MCP_ASYNC_SPLIT: &str = "deliberate transport split, not an omitted implementation: the \
MCP tool returns a durable background receipt because a model call can outlive its client \
transport, while this in-process binding calls the transport-neutral MemoryService \
synchronously and returns the committed ids plus skipped count directly. It therefore has \
no MCP request_id/state/reused receipt; extraction_status is separately exempted for the \
same reason (#1839)";
#[allow(
dead_code,
reason = "zero known gaps is the target state, not a reason to drop the word"
)]
const KNOWN_GAP: &str = "KNOWN GAP — NOT a deliberate unwrap: this binding really does lose \
the field, and the loss predates the guard that found it. Declared so the guard can be \
green on everything else while the gap stays visible in one place; delete this entry \
with the fix, never renew it";
const SHAPE_DIVERGENCES: &[ShapeDivergence] = &[
ShapeDivergence {
binding: "velesdb-node",
tool: "entity",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "feedback",
field: "id_str",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "forget",
field: "found",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "forget",
field: "id_str",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "recall_fused",
field: "dated_context",
reason: DATED_SPLIT,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "recall_fused",
field: "memories",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "recall_where",
field: "memories",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "relate",
field: "edge_id",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "relate",
field: "edge_id_str",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "remember",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "remember_extracted",
field: "request_id",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "remember_extracted",
field: "state",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "remember_extracted",
field: "reused",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-node",
tool: "save_working_context",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "entity",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "feedback",
field: "id_str",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "forget",
field: "found",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "forget",
field: "id_str",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "recall_where",
field: "memories",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "relate",
field: "edge_id",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "relate",
field: "edge_id_str",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "remember",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "remember_extracted",
field: "request_id",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "remember_extracted",
field: "state",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "remember_extracted",
field: "reused",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-python",
tool: "save_working_context",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "entity",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "remember_extracted",
field: "request_id",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "remember_extracted",
field: "state",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "remember_extracted",
field: "reused",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "forget",
field: "found",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "forget",
field: "id_str",
reason: FORGET_BOOL,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "recall_fused",
field: "dated_context",
reason: DATED_SPLIT,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "recall_fused",
field: "memories",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "recall_fused",
field: "now",
reason: DATED_SPLIT,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "recall_where",
field: "memories",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "relate",
field: "edge_id",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "relate",
field: "edge_id_str",
reason: SINGLE_MEMBER,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "remember",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: "velesdb-wasm",
tool: "save_working_context",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: TYPESCRIPT_SDK,
tool: "entity",
field: "id_str",
reason: ID_TWIN,
},
ShapeDivergence {
binding: TYPESCRIPT_SDK,
tool: "remember_extracted",
field: "request_id",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: TYPESCRIPT_SDK,
tool: "remember_extracted",
field: "state",
reason: MCP_ASYNC_SPLIT,
},
ShapeDivergence {
binding: TYPESCRIPT_SDK,
tool: "remember_extracted",
field: "reused",
reason: MCP_ASYNC_SPLIT,
},
];
fn shape_divergence_for(
binding: &str,
tool: &str,
field: &str,
) -> Option<&'static ShapeDivergence> {
SHAPE_DIVERGENCES
.iter()
.find(|d| d.binding == binding && d.tool == tool && d.field == field)
}
async fn connected() -> (TempDir, RunningService<RoleClient, ()>) {
let store_dir = tempfile::tempdir().expect("create scratch store dir");
let embedder: DynEmbedder = Box::new(HashEmbedder::new(DEFAULT_DIMENSION));
let service =
MemoryService::open(store_dir.path(), embedder).expect("open scratch memory store");
let (server_side, client_side) = tokio::io::duplex(1 << 20);
tokio::spawn(async move {
if let Ok(running) = McpServer::new(service).serve(server_side).await {
let _ = running.waiting().await;
}
});
let client = ().serve(client_side).await.expect("MCP initialize handshake over duplex");
(store_dir, client)
}
fn workspace_root() -> PathBuf {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
manifest
.ancestors()
.nth(2)
.expect("velesdb-memory sits two levels under the workspace root")
.to_path_buf()
}
fn surface_block(binding: &Binding) -> String {
let path = workspace_root().join(binding.path);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {} ({}): {err}", binding.path, path.display()));
let mut lines = source
.lines()
.skip_while(|l| l.trim() != binding.surface_impl);
assert!(
lines.next().is_some(),
"{} no longer contains `{}` — the parity guard reads that block to know what the \
binding publishes; point `Binding::surface_impl` at the renamed block",
binding.path,
binding.surface_impl,
);
lines
.take_while(|l| *l != "}")
.collect::<Vec<_>>()
.join("\n")
}
fn published_methods(binding: &Binding) -> BTreeSet<String> {
let block = surface_block(binding);
let methods: BTreeSet<String> = block
.lines()
.filter_map(|line| method_name(line.trim()))
.collect();
assert!(
methods.contains("remember"),
"{}: the surface block parsed to {} method(s) and none of them is `remember` — the \
scan is broken, not the binding",
binding.name,
methods.len(),
);
methods
}
fn method_name(trimmed: &str) -> Option<String> {
let rest = trimmed
.strip_prefix("pub fn ")
.or_else(|| trimmed.strip_prefix("fn "))?;
let (name, _) = rest.split_once('(')?;
(!name.is_empty()).then(|| name.to_owned())
}
const SDK_SOURCE: &str = "sdks/typescript/src/memory.ts";
const SDK_CLASS: &str = "export class MemoryService {";
const SDK_INTERFACE: &str = "interface WasmMemoryServiceInstance {";
const TYPESCRIPT_SDK: &str = "typescript-sdk";
struct SdkShapeSkip {
tool: &'static str,
return_type: &'static str,
reason: &'static str,
}
const SDK_SHAPE_SKIPS: &[SdkShapeSkip] = &[
SdkShapeSkip {
tool: "forget",
return_type: "boolean",
reason: FORGET_BOOL,
},
SdkShapeSkip {
tool: "recall",
return_type: "MemoryRecollection[]",
reason: SINGLE_MEMBER,
},
SdkShapeSkip {
tool: "recall_fused",
return_type: "MemoryRecollection[]",
reason: DATED_SPLIT,
},
SdkShapeSkip {
tool: "recall_where",
return_type: "MemoryRecollection[]",
reason: SINGLE_MEMBER,
},
SdkShapeSkip {
tool: "relate",
return_type: "string",
reason: ID_TWIN,
},
SdkShapeSkip {
tool: "remember",
return_type: "string",
reason: ID_TWIN,
},
SdkShapeSkip {
tool: "save_working_context",
return_type: "string",
reason: ID_TWIN,
},
];
fn typescript_method(line: &str) -> Option<&str> {
let declaration = line.strip_prefix(" ")?;
if declaration.starts_with(' ') || declaration.starts_with("private ") {
return None;
}
let (name, _) = declaration.split_once('(')?;
if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return None;
}
Some(name)
}
fn sdk_methods(opening: &str) -> BTreeSet<String> {
let path = workspace_root().join(SDK_SOURCE);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {SDK_SOURCE} ({}): {err}", path.display()));
let mut lines = source.lines().skip_while(|l| l.trim() != opening);
assert!(
lines.next().is_some(),
"{SDK_SOURCE} no longer contains `{opening}` — the SDK parity check reads that block to \
know what the SDK publishes; point the constant at the renamed block",
);
let methods: BTreeSet<String> = lines
.take_while(|l| *l != "}")
.filter_map(typescript_method)
.map(str::to_owned)
.collect();
assert!(
methods.contains("remember"),
"{SDK_SOURCE} `{opening}` parsed to {} method(s) and none of them is `remember` — the \
scan is broken, not the SDK",
methods.len(),
);
methods
}
fn js_name(tool: &str) -> String {
let mut out = String::with_capacity(tool.len());
let mut capitalize = false;
for c in tool.chars() {
if c == '_' {
capitalize = true;
} else if capitalize {
out.extend(c.to_uppercase());
capitalize = false;
} else {
out.push(c);
}
}
out
}
fn sdk_gaps(
tools: &BTreeSet<String>,
upstream: &BTreeSet<String>,
class: &BTreeSet<String>,
interface: &BTreeSet<String>,
) -> Vec<String> {
let mut gaps = Vec::new();
for tool in tools {
if !upstream.contains(tool) {
continue;
}
let published = js_name(tool);
let in_class = class.contains(&published);
let in_interface = interface.contains(&published);
if in_class && in_interface {
continue;
}
let where_missing = match (in_class, in_interface) {
(false, false) => "neither the class nor the interface",
(true, false) => "the class but NOT the interface (`ensureCapability` keys off it)",
(false, true) => "the interface but NOT the class (no caller can reach it)",
(true, true) => unreachable!("handled above"),
};
gaps.push(format!(
" `{tool}` reaches velesdb-wasm and the SDK declares `{published}` in \
{where_missing}"
));
}
gaps
}
fn sdk_method_regions() -> BTreeMap<String, String> {
let path = workspace_root().join(SDK_SOURCE);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {SDK_SOURCE} ({}): {err}", path.display()));
let mut lines = source.lines().skip_while(|line| line.trim() != SDK_CLASS);
assert!(
lines.next().is_some(),
"{SDK_SOURCE} no longer contains `{SDK_CLASS}`"
);
let block: Vec<&str> = lines.take_while(|line| *line != "}").collect();
let starts: Vec<(usize, String)> = block
.iter()
.enumerate()
.filter_map(|(index, line)| typescript_method(line).map(|name| (index, name.to_owned())))
.collect();
let mut regions = BTreeMap::new();
for (position, (start, name)) in starts.iter().enumerate() {
let end = starts
.get(position + 1)
.map_or(block.len(), |(index, _)| *index);
regions.insert(name.clone(), block[*start..end].join("\n"));
}
regions
}
fn promise_return_type(region: &str) -> Option<String> {
let flat = region.split_whitespace().collect::<Vec<_>>().join(" ");
let (_, tail) = flat.split_once("): Promise<")?;
let mut depth = 1_u32;
for (index, character) in tail.char_indices() {
match character {
'<' => depth += 1,
'>' => depth = depth.saturating_sub(1),
_ => {}
}
if depth == 0 {
return Some(tail[..index].trim().to_owned());
}
}
None
}
fn sdk_return_types() -> BTreeMap<String, String> {
sdk_method_regions()
.into_iter()
.filter_map(|(name, region)| {
promise_return_type(®ion).map(|return_type| (name, return_type))
})
.collect()
}
fn typescript_interface_name(line: &str) -> Option<&str> {
let declaration = line
.strip_prefix("export interface ")
.or_else(|| line.strip_prefix("interface "))?;
let name = declaration
.split(|character: char| character.is_whitespace() || character == '{')
.next()?;
(!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')).then_some(name)
}
fn typescript_field(line: &str) -> Option<String> {
let declaration = line.strip_prefix(" ")?;
if declaration.starts_with(' ') {
return None;
}
let (name, _) = declaration.split_once(':')?;
let name = name.trim_end_matches('?');
name.chars()
.all(|c| c.is_alphanumeric() || c == '_')
.then(|| name.to_owned())
}
fn sdk_interfaces() -> BTreeMap<String, BTreeSet<String>> {
let path = workspace_root().join(SDK_SOURCE);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {SDK_SOURCE} ({}): {err}", path.display()));
let lines: Vec<&str> = source.lines().collect();
let declarations: Vec<(usize, &str)> = lines
.iter()
.enumerate()
.filter_map(|(index, line)| typescript_interface_name(line).map(|name| (index, name)))
.collect();
declarations
.into_iter()
.map(|(start, name)| {
let fields = lines[start + 1..]
.iter()
.take_while(|line| **line != "}")
.filter_map(|line| typescript_field(line))
.collect();
(name.to_owned(), fields)
})
.collect()
}
fn sdk_shape_skip_for(tool: &str, return_type: &str) -> Option<&'static SdkShapeSkip> {
SDK_SHAPE_SKIPS
.iter()
.find(|skip| skip.tool == tool && skip.return_type == return_type)
}
fn sdk_names_field(fields: &BTreeSet<String>, wire_field: &str) -> bool {
fields.contains(wire_field) || fields.contains(&js_name(wire_field))
}
fn sdk_output_gaps(
tool: &str,
output_fields: &BTreeSet<String>,
sdk_fields: &BTreeSet<String>,
) -> Vec<String> {
output_fields
.iter()
.filter(|field| {
!sdk_names_field(sdk_fields, field)
&& shape_divergence_for(TYPESCRIPT_SDK, tool, field).is_none()
})
.map(|field| {
format!(" {tool}.{field} is not named by the TypeScript SDK return interface")
})
.collect()
}
fn sdk_shape_findings(tools: &[rmcp::model::Tool], upstream: &BTreeSet<String>) -> Vec<String> {
let returns = sdk_return_types();
let interfaces = sdk_interfaces();
let mut findings = Vec::new();
for tool in tools {
let name = tool.name.as_ref();
if !upstream.contains(name) {
continue;
}
let Some(return_type) = returns.get(&js_name(name)) else {
continue; };
if let Some(fields) = interfaces.get(return_type) {
findings.extend(sdk_output_gaps(name, &output_root_fields(tool), fields));
} else if sdk_shape_skip_for(name, return_type).is_none() {
findings.push(format!(
" {name} returns non-local `{return_type}` without an explicit SDK_SHAPE_SKIPS decision"
));
}
}
findings
}
fn stale_sdk_skip_reason(
skip: &SdkShapeSkip,
tools: &BTreeSet<String>,
upstream: &BTreeSet<String>,
returns: &BTreeMap<String, String>,
interfaces: &BTreeMap<String, BTreeSet<String>>,
) -> Option<String> {
if skip.reason.trim().is_empty() {
return Some("the reason is empty".to_owned());
}
if !tools.contains(skip.tool) || !upstream.contains(skip.tool) {
return Some("the tool no longer reaches the TypeScript SDK".to_owned());
}
let method = js_name(skip.tool);
let Some(actual) = returns.get(&method) else {
return Some(format!(
"the SDK no longer declares `{method}` with a Promise return"
));
};
if actual != skip.return_type {
return Some(format!(
"the return changed from `{}` to `{actual}`",
skip.return_type
));
}
interfaces
.contains_key(actual)
.then(|| format!("`{actual}` is now a local interface and must be checked"))
}
fn method_regions(binding: &Binding) -> BTreeMap<String, String> {
let block = surface_block(binding);
let lines: Vec<&str> = block.lines().collect();
let mut regions = BTreeMap::new();
let mut start = 0usize;
for (index, line) in lines.iter().enumerate() {
let Some(name) = method_name(line.trim()) else {
continue;
};
if index < start {
continue;
}
let end = lines[index..]
.iter()
.position(|l| *l == " }")
.map_or(lines.len(), |offset| index + offset + 1);
regions.insert(name, lines[start..end].join("\n"));
start = end;
}
assert!(
regions.contains_key("remember"),
"{}: the region scan produced {} method(s) and none of them is `remember` — the \
scan is broken, not the binding",
binding.name,
regions.len(),
);
regions
}
fn output_window(region: &str, method: &str) -> String {
cut_parameter_list(&cut_signature_attribute(region), method)
}
fn cut_signature_attribute(region: &str) -> String {
let mut text = region.to_owned();
while let Some(at) = text.find("signature = (") {
let open = at + "signature = ".len();
let Some(close) = matching_paren(&text[open..]).map(|offset| open + offset) else {
break;
};
text.replace_range(open..=close, "");
}
text
}
fn cut_parameter_list(region: &str, method: &str) -> String {
let mut at = 0usize;
let mut declaration = None;
for line in region.split_inclusive('\n') {
if method_name(line.trim()).as_deref() == Some(method) {
declaration = Some(at);
break;
}
at += line.len();
}
let Some(header) = declaration else {
return region.to_owned();
};
let Some(open) = region[header..].find('(').map(|offset| header + offset) else {
return region.to_owned();
};
let Some(close) = matching_paren(®ion[open..]).map(|offset| open + offset) else {
return region.to_owned();
};
let mut window = region[..open].to_owned();
window.push_str(®ion[close + 1..]);
window
}
fn without_doc_comments(region: &str) -> String {
region
.lines()
.filter(|line| !line.trim_start().starts_with("///"))
.collect::<Vec<_>>()
.join("\n")
}
fn matching_paren(text: &str) -> Option<usize> {
let mut depth = 0usize;
for (offset, character) in text.char_indices() {
match character {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(offset);
}
}
_ => {}
}
}
None
}
fn output_root_fields(tool: &rmcp::model::Tool) -> BTreeSet<String> {
tool.output_schema
.as_ref()
.and_then(|schema| schema.get("properties"))
.and_then(serde_json::Value::as_object)
.map(|props| props.keys().cloned().collect())
.unwrap_or_default()
}
fn input_root_fields(tool: &rmcp::model::Tool) -> BTreeSet<String> {
tool.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.map(|props| props.keys().cloned().collect())
.unwrap_or_default()
}
fn input_window(region: &str, method: &str) -> String {
let Some(header) = region
.lines()
.position(|line| method_name(line.trim()).as_deref() == Some(method))
else {
return String::new();
};
let declaration = region.lines().skip(header).collect::<Vec<_>>().join("\n");
let Some(open) = declaration.find('(') else {
return String::new();
};
let Some(close) = matching_paren(&declaration[open..]).map(|offset| open + offset) else {
return String::new();
};
declaration[open + 1..close].to_owned()
}
#[tokio::test]
async fn recall_count_input_is_k_on_the_server_and_every_binding() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
let recall_tools = ["recall", "recall_where", "recall_fused"];
let mut gaps = Vec::new();
for name in recall_tools {
let tool = tools
.iter()
.find(|tool| tool.name.as_ref() == name)
.unwrap_or_else(|| panic!("live server no longer advertises `{name}`"));
let fields = input_root_fields(tool);
if !fields.contains("k") || fields.contains("limit") {
gaps.push(format!(
" {name} advertises {fields:?}, expected canonical `k`"
));
}
for binding in BINDINGS {
let regions = method_regions(binding);
let params = regions
.get(name)
.map_or_else(String::new, |region| input_window(region, name));
if !names_identifier(¶ms, "k") {
gaps.push(format!(" {}.{name} does not accept `k`", binding.name));
}
}
}
assert!(
gaps.is_empty(),
"shared recall count input drifted across surfaces:\n{}",
gaps.join("\n")
);
client.cancel().await.expect("close the MCP session");
}
const SERVER_TOOL_SOURCES: &[&str] = &[
"crates/velesdb-memory/src/mcp.rs",
"crates/velesdb-memory/src/mcp/advanced_tools.rs",
"crates/velesdb-memory/src/mcp/context_tools.rs",
];
fn server_output_types() -> BTreeMap<String, String> {
let mut types = BTreeMap::new();
for relative in SERVER_TOOL_SOURCES {
let path = workspace_root().join(relative);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {} ({}): {err}", relative, path.display()));
collect_output_types(&source, &mut types);
}
assert!(
types.len() >= 20,
"only {} tool output type(s) parsed out of the server source — the scan is broken, \
not the server (it publishes 27 tools)",
types.len(),
);
types
}
fn collect_output_types(source: &str, types: &mut BTreeMap<String, String>) {
let mut pending: Option<String> = None;
for line in source.lines() {
let trimmed = line.trim();
if let Some(tool) = between(trimmed, "name = \"", "\"") {
pending = Some(tool.to_owned());
} else if let Some(ty) = between(trimmed, "wire_safe_output_schema::<", ">") {
if let Some(tool) = pending.take() {
types.insert(tool, ty.to_owned());
}
}
}
}
fn between<'a>(haystack: &'a str, open: &str, close: &str) -> Option<&'a str> {
let rest = haystack.split_once(open)?.1;
let (inner, _) = rest.split_once(close)?;
(!inner.is_empty()).then_some(inner)
}
fn names_identifier(region: &str, needle: &str) -> bool {
region.match_indices(needle).any(|(at, _)| {
!bounded_by_ident_char(®ion[..at], true)
&& !bounded_by_ident_char(®ion[at + needle.len()..], false)
})
}
fn bounded_by_ident_char(text: &str, before: bool) -> bool {
let adjacent = if before {
text.chars().next_back()
} else {
text.chars().next()
};
adjacent.is_some_and(|c| c.is_alphanumeric() || c == '_')
}
fn binding_structs(binding: &Binding) -> BTreeMap<String, String> {
let crate_src = workspace_root()
.join(binding.path)
.parent()
.expect("a binding surface file lives in the crate's src/")
.to_path_buf();
let mut structs = BTreeMap::new();
for entry in std::fs::read_dir(&crate_src).expect("read the binding crate's src/") {
let path = entry.expect("read a src/ entry").path();
if path.extension().is_some_and(|ext| ext == "rs") {
let source = std::fs::read_to_string(&path).expect("read a binding source file");
collect_structs(&source, &mut structs);
}
}
structs
}
fn collect_structs(source: &str, structs: &mut BTreeMap<String, String>) {
let lines: Vec<&str> = source.lines().collect();
for (index, line) in lines.iter().enumerate() {
let Some(name) = between(line, "struct ", " {") else {
continue;
};
if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
continue; }
let end = lines[index..]
.iter()
.position(|l| *l == "}")
.map_or(lines.len(), |offset| index + offset);
structs.insert(name.to_owned(), lines[index..end].join("\n"));
}
}
fn region_with_named_structs(region: &str, structs: &BTreeMap<String, String>) -> String {
let mut text = region.to_owned();
for (name, body) in structs {
if names_identifier(region, name) {
text.push('\n');
text.push_str(body);
}
}
text
}
fn exemption_for(binding: &str, tool: &str) -> Option<&'static Exemption> {
EXEMPTIONS
.iter()
.find(|e| e.binding == binding && e.tool == tool)
}
#[tokio::test]
async fn every_mcp_tool_is_implemented_or_exempted_in_every_binding() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
assert!(!tools.is_empty(), "the server advertises at least one tool");
let mut gaps: Vec<String> = Vec::new();
for binding in BINDINGS {
let methods = published_methods(binding);
for tool in &tools {
let name = tool.name.as_ref();
if methods.contains(name) || exemption_for(binding.name, name).is_some() {
continue;
}
gaps.push(format!(" {name} is missing from {}", binding.name));
}
}
assert!(
gaps.is_empty(),
"{} MCP tool(s) unreachable from a binding:\n{}\n\nEvery MCP tool must be either \
implemented in the binding (relay `MemoryService`, add no logic — follow the idiom of \
a neighbouring tool such as `why`) or declared in `EXEMPTIONS` in this file WITH the \
reason it does not apply there. Silence is not a decision: the `entity` tool was \
invisible from all three bindings for its whole life precisely because nothing \
compared the surfaces.",
gaps.len(),
gaps.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
#[tokio::test]
async fn no_exemption_is_stale() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
let advertised: BTreeSet<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
let mut stale: Vec<String> = Vec::new();
for exemption in EXEMPTIONS {
let Some(binding) = BINDINGS.iter().find(|b| b.name == exemption.binding) else {
stale.push(format!(
" unknown binding `{}` (exempting `{}`)",
exemption.binding, exemption.tool
));
continue;
};
if !advertised.contains(exemption.tool) {
stale.push(format!(
" `{}` is no longer an MCP tool (exempted on {}: {})",
exemption.tool, exemption.binding, exemption.reason
));
} else if published_methods(binding).contains(exemption.tool) {
stale.push(format!(
" {} now implements `{}` — drop the exemption (it claimed: {})",
exemption.binding, exemption.tool, exemption.reason
));
}
}
assert!(
stale.is_empty(),
"{} stale exemption(s) in EXEMPTIONS:\n{}",
stale.len(),
stale.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
#[tokio::test]
async fn every_output_field_is_relayed_or_divergence_is_declared_in_every_binding() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
let server_types = server_output_types();
let mut gaps: Vec<String> = Vec::new();
for binding in BINDINGS {
let regions = method_regions(binding);
let structs = binding_structs(binding);
for tool in &tools {
let name = tool.name.as_ref();
let Some(region) = regions.get(name) else {
continue; };
if server_types
.get(name)
.is_some_and(|ty| names_identifier(region, ty))
{
continue; }
let window = output_window(region, name);
let declared = region_with_named_structs(&window, &structs);
for field in output_root_fields(tool) {
if names_identifier(&declared, &field)
|| shape_divergence_for(binding.name, name, &field).is_some()
{
continue;
}
gaps.push(format!(
" {}.{field} is not relayed by {}",
name, binding.name
));
}
}
}
assert!(
gaps.is_empty(),
"{} output field(s) the server publishes but a binding never names:\n{}\n\nEvery \
root field of a tool's output_schema must reach the binding: name the server's \
output type, or name the field in the method region (doc comment, attributes or \
body), or declare the drop in SHAPE_DIVERGENCES in this file WITH its reason. This \
is the invariant that was missing when `load_working_context` served \
`{{found, working, other_sessions}}` for months while all three bindings returned a \
bare `WorkingContext | null`: the name guard was green, and nobody was looking at \
the shape.",
gaps.len(),
gaps.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
const RELAY_BY_TYPE_ONLY: &[&str] = &["load_working_context"];
#[tokio::test]
async fn envelope_tools_are_relayed_by_type_never_by_prose_alone() {
let server_types = server_output_types();
let mut weak: Vec<String> = Vec::new();
for binding in BINDINGS {
let regions = method_regions(binding);
for tool in RELAY_BY_TYPE_ONLY {
let Some(region) = regions.get(*tool) else {
continue; };
let ty = server_types
.get(*tool)
.unwrap_or_else(|| panic!("no server output type parsed for `{tool}`"));
if !names_identifier(region, ty) {
weak.push(format!(
" {}.{tool} never names `{ty}` — it relays the envelope without \
declaring its type",
binding.name
));
}
}
}
assert!(
weak.is_empty(),
"{} binding(s) satisfy the shape guard only by text search:\n{}\n\nThese tools must \
name the server's own output type (route 1), because a doc comment describing the \
envelope is enough to satisfy route 2 while the body returns the bare form — which \
is exactly the drift that went unnoticed for months. Bind the value with an explicit \
annotation, e.g. `let loaded: LoadedWorkingContext = \
svc.resume_working_context(..)?;`, so the compiler enforces what this test reads.",
weak.len(),
weak.join("\n"),
);
}
#[tokio::test]
async fn no_shape_divergence_is_stale() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
let mut stale: Vec<String> = Vec::new();
for divergence in SHAPE_DIVERGENCES {
if let Some(reason) = stale_shape_reason(&tools, divergence) {
stale.push(format!(
" {} / {} / {}: {reason} (it claimed: {})",
divergence.binding, divergence.tool, divergence.field, divergence.reason
));
}
}
assert!(
stale.is_empty(),
"{} stale entry(ies) in SHAPE_DIVERGENCES:\n{}",
stale.len(),
stale.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
fn stale_shape_reason(tools: &[rmcp::model::Tool], divergence: &ShapeDivergence) -> Option<String> {
if divergence.binding == TYPESCRIPT_SDK {
return stale_sdk_shape_reason(tools, divergence);
}
if !BINDINGS.iter().any(|b| b.name == divergence.binding) {
return Some(format!("unknown binding `{}`", divergence.binding));
}
let Some(tool) = tools.iter().find(|t| t.name.as_ref() == divergence.tool) else {
return Some(format!("`{}` is no longer an MCP tool", divergence.tool));
};
if !output_root_fields(tool).contains(divergence.field) {
return Some(format!(
"`{}` no longer has a root output field `{}`",
divergence.tool, divergence.field
));
}
let binding = BINDINGS.iter().find(|b| b.name == divergence.binding)?;
let regions = method_regions(binding);
let region = regions.get(divergence.tool)?;
let server_type = server_output_types().get(divergence.tool).cloned();
if server_type
.as_ref()
.is_some_and(|ty| names_identifier(region, ty))
{
return Some(format!(
"the binding now relays the server type `{}` wholesale",
server_type.unwrap_or_default()
));
}
let window = without_doc_comments(&output_window(region, divergence.tool));
let declared = region_with_named_structs(&window, &binding_structs(binding));
names_identifier(&declared, divergence.field)
.then(|| format!("the binding now names `{}`", divergence.field))
}
fn stale_sdk_shape_reason(
tools: &[rmcp::model::Tool],
divergence: &ShapeDivergence,
) -> Option<String> {
let Some(tool) = tools
.iter()
.find(|tool| tool.name.as_ref() == divergence.tool)
else {
return Some(format!("`{}` is no longer an MCP tool", divergence.tool));
};
if !output_root_fields(tool).contains(divergence.field) {
return Some(format!(
"`{}` no longer has a root output field `{}`",
divergence.tool, divergence.field
));
}
let method = js_name(divergence.tool);
let returns = sdk_return_types();
let Some(return_type) = returns.get(&method) else {
return Some(format!(
"the SDK no longer declares `{method}` with a Promise return"
));
};
let interfaces = sdk_interfaces();
let Some(fields) = interfaces.get(return_type) else {
return Some(format!(
"`{method}` now returns non-local `{return_type}`; use SDK_SHAPE_SKIPS"
));
};
sdk_names_field(fields, divergence.field)
.then(|| format!("the SDK now names `{}`", divergence.field))
}
const SYNTHETIC_REGION: &str = "\
/// Fetch the source back.
#[pyo3(signature = (handle, media = None))]
fn retrieve_context_source(&self, py: Python<'_>, handle: &str, media: Option<(u8, u8)>) -> PyResult<Py<PyAny>> {
let source: ContextSource = fetch();
Ok(source)
}
";
#[test]
fn the_raw_region_is_satisfied_by_a_parameter_name() {
assert!(
names_identifier(SYNTHETIC_REGION, "handle"),
"fixture precondition: the raw region names `handle`",
);
}
#[test]
fn the_output_window_refuses_a_field_named_only_by_an_input() {
let window = output_window(SYNTHETIC_REGION, "retrieve_context_source");
assert!(
!names_identifier(&window, "handle"),
"an input named `handle` still satisfies the window:\n{window}",
);
assert!(
!names_identifier(&window, "media"),
"the `signature` attribute still spells the parameters:\n{window}",
);
}
#[test]
fn the_output_window_keeps_everything_that_describes_the_return() {
let window = output_window(SYNTHETIC_REGION, "retrieve_context_source");
for kept in ["PyResult", "ContextSource", "Fetch"] {
assert!(
names_identifier(&window, kept),
"the cut swallowed `{kept}`, which describes the return:\n{window}",
);
}
}
#[test]
fn the_cut_falls_back_to_the_whole_region_when_the_declaration_is_absent() {
let missed = cut_parameter_list(SYNTHETIC_REGION, "a_method_that_is_not_here");
assert_eq!(missed, SYNTHETIC_REGION);
}
#[test]
fn the_cut_stops_at_the_paren_that_closes_the_parameter_list() {
let window = output_window(SYNTHETIC_REGION, "retrieve_context_source");
assert!(
window.contains("fn retrieve_context_source -> PyResult<Py<PyAny>> {"),
"the declaration did not survive the cut intact:\n{window}",
);
}
const PROSE_ONLY_REGION: &str = "\
/// Returns caller memories ONLY: entity hubs and the context compiler's\n\
/// artefacts are internal scaffolding and never come back through this call.\n\
fn recall_where(&self, py: Python<'_>) -> PyResult<Vec<PyObject>> {\n\
Ok(vec![])\n\
}\n\
";
#[test]
fn the_raw_window_is_satisfied_by_prose_alone() {
let window = output_window(PROSE_ONLY_REGION, "recall_where");
assert!(
names_identifier(&window, "memories"),
"fixture precondition: the raw window names `memories` only in prose",
);
}
#[test]
fn stripping_doc_comments_refuses_a_field_named_only_in_prose() {
let window = without_doc_comments(&output_window(PROSE_ONLY_REGION, "recall_where"));
assert!(
!names_identifier(&window, "memories"),
"a field named only in a doc comment must not survive the strip:\n{window}",
);
}
#[test]
fn stripping_doc_comments_keeps_everything_that_describes_the_return() {
let window = without_doc_comments(&output_window(PROSE_ONLY_REGION, "recall_where"));
for kept in ["PyResult", "PyObject", "recall_where"] {
assert!(
names_identifier(&window, kept),
"the strip swallowed `{kept}`, which is not a doc comment:\n{window}",
);
}
}
#[tokio::test]
async fn the_typescript_sdk_relays_every_tool_that_reaches_the_wasm_binding() {
let (_store, client) = connected().await;
let tools: BTreeSet<String> = client
.list_all_tools()
.await
.expect("list tools")
.iter()
.map(|t| t.name.to_string())
.collect();
let wasm = BINDINGS
.iter()
.find(|b| b.name == "velesdb-wasm")
.expect("velesdb-wasm is a declared binding");
let gaps = sdk_gaps(
&tools,
&published_methods(wasm),
&sdk_methods(SDK_CLASS),
&sdk_methods(SDK_INTERFACE),
);
assert!(
gaps.is_empty(),
"{} tool(s) reach velesdb-wasm but stop at the TypeScript SDK:\n{}\n\nThe SDK calls \
into the WASM binding, so it can never publish more than that binding does — but it \
can publish LESS, silently, and it did: `entity` and `unrelate` were absent for the \
SDK's whole life while wasm exposed both. Add the method to BOTH the class and the \
`WasmMemoryServiceInstance` interface in {}.",
gaps.len(),
gaps.join("\n"),
SDK_SOURCE,
);
client.cancel().await.expect("close the MCP session");
}
#[tokio::test]
async fn the_typescript_sdk_relays_every_checkable_output_shape() {
let (_store, client) = connected().await;
let tools = client.list_all_tools().await.expect("list tools");
let wasm = BINDINGS
.iter()
.find(|binding| binding.name == "velesdb-wasm")
.expect("velesdb-wasm is a declared binding");
let findings = sdk_shape_findings(&tools, &published_methods(wasm));
assert!(
findings.is_empty(),
"{} TypeScript SDK output-shape finding(s):\n{}\n\nA local Promise<X> return must name \
every root key of the live MCP output schema in interface X (snake_case or camelCase). \
Fix the SDK field, declare a deliberate field loss in SHAPE_DIVERGENCES WITH its \
reason, or pin an uncheckable non-local return in SDK_SHAPE_SKIPS WITH its reason.",
findings.len(),
findings.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
#[test]
fn the_sdk_shape_parser_reads_real_returns_and_interfaces() {
let returns = sdk_return_types();
assert_eq!(
returns.get("why").map(String::as_str),
Some("MemoryExplanation")
);
assert_eq!(returns.get("remember").map(String::as_str), Some("string"));
let interfaces = sdk_interfaces();
let explanation = interfaces
.get("MemoryExplanation")
.expect("parse MemoryExplanation");
assert!(explanation.contains("nodes") && explanation.contains("truncated"));
}
#[test]
fn the_sdk_shape_check_names_the_field_a_stripped_interface_lost() {
let output = ["edges", "nodes", "truncated"]
.map(str::to_owned)
.into_iter()
.collect();
let stripped = ["edges", "nodes"].map(str::to_owned).into_iter().collect();
let gaps = sdk_output_gaps("why", &output, &stripped);
assert_eq!(
gaps.len(),
1,
"exactly the stripped field is reported: {gaps:?}"
);
assert!(gaps[0].contains("why.truncated"), "got: {}", gaps[0]);
}
#[test]
fn the_sdk_shape_check_is_silent_for_a_complete_interface() {
let output = ["edges", "nodes", "truncated"]
.map(str::to_owned)
.into_iter()
.collect();
assert!(
sdk_output_gaps("why", &output, &output).is_empty(),
"a complete local return interface must produce no finding",
);
}
#[test]
fn the_sdk_shape_check_accepts_the_camel_case_wire_spelling() {
let output = ["relations_in"].map(str::to_owned).into_iter().collect();
let sdk = ["relationsIn"].map(str::to_owned).into_iter().collect();
assert!(sdk_output_gaps("entity", &output, &sdk).is_empty());
}
#[tokio::test]
async fn no_sdk_shape_skip_is_stale() {
let (_store, client) = connected().await;
let listed = client.list_all_tools().await.expect("list tools");
let tools = listed
.iter()
.map(|tool| tool.name.to_string())
.collect::<BTreeSet<_>>();
let wasm = BINDINGS
.iter()
.find(|binding| binding.name == "velesdb-wasm")
.expect("velesdb-wasm is a declared binding");
let upstream = published_methods(wasm);
let returns = sdk_return_types();
let interfaces = sdk_interfaces();
let stale: Vec<String> = SDK_SHAPE_SKIPS
.iter()
.filter_map(|skip| {
stale_sdk_skip_reason(skip, &tools, &upstream, &returns, &interfaces)
.map(|reason| format!(" {} -> {}: {reason}", skip.tool, skip.return_type))
})
.collect();
assert!(
stale.is_empty(),
"{} stale SDK_SHAPE_SKIPS entry(ies):\n{}",
stale.len(),
stale.join("\n"),
);
client.cancel().await.expect("close the MCP session");
}
#[test]
fn the_sdk_check_names_the_method_a_stripped_sdk_lost() {
let tools = ["remember", "unrelate", "recall_fused_dated"]
.map(str::to_owned)
.into_iter()
.collect::<BTreeSet<_>>();
let upstream = tools.clone();
let stripped = ["remember", "recallFusedDated"]
.map(str::to_owned)
.into_iter()
.collect::<BTreeSet<_>>();
let gaps = sdk_gaps(&tools, &upstream, &stripped, &stripped);
assert_eq!(
gaps.len(),
1,
"exactly the dropped tool is reported: {gaps:?}"
);
assert!(
gaps[0].contains("`unrelate`"),
"the refusal names the missing tool, got: {}",
gaps[0],
);
}
#[test]
fn the_sdk_check_is_silent_when_every_tool_is_relayed() {
let tools = ["remember", "unrelate", "recall_fused_dated"]
.map(str::to_owned)
.into_iter()
.collect::<BTreeSet<_>>();
let complete = ["remember", "unrelate", "recallFusedDated"]
.map(str::to_owned)
.into_iter()
.collect::<BTreeSet<_>>();
assert!(
sdk_gaps(&tools, &tools, &complete, &complete).is_empty(),
"a complete SDK must produce no finding",
);
}
#[test]
fn the_sdk_check_ignores_a_tool_that_never_reaches_wasm() {
let tools = ["remember", "feedback"]
.map(str::to_owned)
.into_iter()
.collect::<BTreeSet<_>>();
let upstream = ["remember"].map(str::to_owned).into_iter().collect();
let sdk = ["remember"].map(str::to_owned).into_iter().collect();
assert!(
sdk_gaps(&tools, &upstream, &sdk, &sdk).is_empty(),
"`feedback` is exempted on wasm; the SDK must not be asked for it",
);
}
#[test]
fn the_sdk_check_distinguishes_the_class_from_the_interface() {
let tools = ["entity"].map(str::to_owned).into_iter().collect();
let upstream = ["entity"].map(str::to_owned).into_iter().collect();
let present: BTreeSet<String> = ["entity"].map(str::to_owned).into_iter().collect();
let absent = BTreeSet::new();
let gaps = sdk_gaps(&tools, &upstream, &present, &absent);
assert!(gaps[0].contains("NOT the interface"), "got: {}", gaps[0]);
let gaps = sdk_gaps(&tools, &upstream, &absent, &present);
assert!(gaps[0].contains("NOT the class"), "got: {}", gaps[0]);
}
const FRAGMENT_SOURCE: &str = "crates/velesdb-memory/src/context/model.rs";
const FRAGMENT_STRUCT: &str = "pub struct ContextFragment {";
const SDK_FRAGMENT: &str = "export interface CompileContextFragment {";
fn rust_struct_fields(source_path: &str, opening: &str) -> BTreeSet<String> {
let path = workspace_root().join(source_path);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {source_path} ({}): {err}", path.display()));
let mut lines = source.lines().skip_while(|l| l.trim() != opening);
assert!(
lines.next().is_some(),
"{source_path} no longer contains `{opening}` — point the constant at the renamed struct",
);
lines
.take_while(|l| *l != "}")
.filter_map(|line| {
let declaration = line.trim().strip_prefix("pub ")?;
let (name, _) = declaration.split_once(':')?;
name.chars()
.all(|c| c.is_alphanumeric() || c == '_')
.then(|| name.to_owned())
})
.collect()
}
fn typescript_interface_fields(opening: &str) -> BTreeSet<String> {
let path = workspace_root().join(SDK_SOURCE);
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read {SDK_SOURCE} ({}): {err}", path.display()));
let mut lines = source.lines().skip_while(|l| l.trim() != opening);
assert!(
lines.next().is_some(),
"{SDK_SOURCE} no longer contains `{opening}` — point the constant at the renamed interface",
);
lines
.take_while(|l| *l != "}")
.filter_map(|line| {
let declaration = line.trim();
let (name, _) = declaration.split_once(':')?;
let name = name.trim_end_matches('?');
name.chars()
.all(|c| c.is_alphanumeric() || c == '_')
.then(|| name.to_owned())
})
.collect()
}
#[test]
fn the_fragment_scans_read_something_on_both_sides() {
let canonical = rust_struct_fields(FRAGMENT_SOURCE, FRAGMENT_STRUCT);
assert!(
canonical.contains("content") && canonical.contains("id"),
"the canonical fragment scan parsed {canonical:?} — the scan is broken, not the struct",
);
let sdk = typescript_interface_fields(SDK_FRAGMENT);
assert!(
sdk.contains("content") && sdk.contains("id"),
"the SDK fragment scan parsed {sdk:?} — the scan is broken, not the SDK",
);
}
const SDK_FRAGMENT_EXEMPTIONS: &[(&str, &str)] = &[(
"path",
"resolving a `path` fragment is a server-side I/O pre-pass gated on \
VELESDB_MEMORY_INGEST_ROOTS, an operator-configured allowlist of \
directories. This SDK runs on the WASM binding, which has neither a \
filesystem nor that setting. NO binding declares it — velesdb-node and \
velesdb-python do not resolve paths either; the MCP daemon is the only \
surface that can honour one, so declaring it here would be a field that \
always fails.",
)];
#[test]
fn every_fragment_exemption_names_a_field_that_actually_exists() {
let canonical = rust_struct_fields(FRAGMENT_SOURCE, FRAGMENT_STRUCT);
for (field, _) in SDK_FRAGMENT_EXEMPTIONS {
assert!(
canonical.contains(*field),
"`{field}` is exempted from the SDK fragment but is not a field of the canonical \
fragment any more — delete the exemption",
);
}
}
#[test]
fn the_typescript_fragment_declares_every_field_the_wire_accepts() {
let canonical = rust_struct_fields(FRAGMENT_SOURCE, FRAGMENT_STRUCT);
let mut sdk = typescript_interface_fields(SDK_FRAGMENT);
sdk.extend(
SDK_FRAGMENT_EXEMPTIONS
.iter()
.map(|(field, _)| (*field).to_owned()),
);
let missing: Vec<&String> = canonical.difference(&sdk).collect();
assert!(
missing.is_empty(),
"the TypeScript SDK's `CompileContextFragment` is missing {missing:?}, so a TypeScript \
caller cannot express input the server accepts. The tool is reachable and half of its \
contract is not — declare the field in {SDK_SOURCE}, or, if it is deliberately withheld, \
say so where a reader will see it rather than leaving the absence to look like an \
oversight.",
);
}