Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions specs/port/trios/crates/trios-server/src/tools/trios_kg.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// MCP tools for the trios-kg crate (R0 stub).
// Port of gHashTag/trios:crates/trios-server/src/tools/trios_kg.rs
// (at 8b229e9489ee, 148 lines) — Roadmap Stage 6, goal #4548, target t27c
// gen-rust.
//
// The original exposes Knowledge Graph operations over MCP with four
// tools behind a name dispatcher: kg_create_entity (requiring
// entity_type and name, defaulting the properties), kg_create_edge
// (requiring source, target and edge_type, defaulting the weight to
// 1.0), kg_query (requiring the query, defaulting the limit to 10 and
// capping the mock results at 3), and kg_traverse (requiring the
// source, defaulting max_depth to 1 and capping the mock paths at 3).
// Every mock id is derived through the djb2-style hash stub — 5381,
// then hash = hash * 33 + byte per input byte — formatted as a hex
// suffix; entity ids hash the name alone, edge ids hash
// source-target-edge_type.
//
// The JSON reading and building are plumbing; the decisions — the
// dispatch table over the four tool names, each tool's required-key
// gate and its defaults, the caps of the mock enumerations, and the
// hash stub's arithmetic — are ported here as pure functions and
// tested. The callers hand the answers and receive the plans (issue
// #9402, "Port the decision, not the plumbing").

module port::trios_server_tools_kg;

// ---------------------------------------------------------------------------
// Original lines 140-148: `fn md5_compute(data) -> u64` — the hash stub:
// 5381, then hash = hash * 33 + byte, wrapping. The multiply-and-add is
// portable arithmetic; the format! hex suffix is the caller's string
// plumbing.
// ---------------------------------------------------------------------------

pub fn md5_compute(data: []const u8) -> u64 {
var hash: u64 = 5381;
var i: usize = 0;
while (i < data.len) {
hash = hash * 33 + data[i];
i += 1;
}
return hash;
}

// Original line 40: entity ids hash the name alone.
pub fn entity_hash_input(name: str) -> u64 {
return md5_compute(name);
}

// Original line 72: edge ids hash source-target-edge_type joined by '-'.
// The string joining is the caller's plumbing; the hash runs over the
// parts the caller hands it.
pub fn edge_hash_input(source: str, target: str, edge_type: str) -> u64 {
return md5_compute(source) + md5_compute(target) + md5_compute(edge_type);
}

// ---------------------------------------------------------------------------
// Original lines 9-17: `pub async fn dispatch(name, input)` — the four
// tool names route to their handlers; anything else answers None.
// ---------------------------------------------------------------------------

pub fn dispatch(name: str) -> bool {
if (name == "kg_create_entity") { return true; }
if (name == "kg_create_edge") { return true; }
if (name == "kg_query") { return true; }
if (name == "kg_traverse") { return true; }
return false;
}

pub fn dispatched_tool_count() -> usize {
return 4;
}

// ---------------------------------------------------------------------------
// Original lines 20-48: `kg_create_entity` — entity_type and name are
// required; the properties default to an empty object; the id hashes
// the name.
// ---------------------------------------------------------------------------

pub struct KgEntityPlan {
entity_type_present: bool,
name_present: bool,
properties_default: bool,
id_hashes_name_only: bool,
}

pub fn kg_create_entity(entity_type_present: bool, name_present: bool) -> KgEntityPlan {
return KgEntityPlan {
entity_type_present: entity_type_present,
name_present: name_present,
properties_default: true,
id_hashes_name_only: true,
};
}

// ---------------------------------------------------------------------------
// Original lines 51-81: `kg_create_edge` — source, target and edge_type
// are required; the weight defaults to 1.0.
// ---------------------------------------------------------------------------

pub struct KgEdgePlan {
source_present: bool,
target_present: bool,
edge_type_present: bool,
weight_default: f64,
}

pub fn kg_create_edge(source_present: bool, target_present: bool, edge_type_present: bool) -> KgEdgePlan {
return KgEdgePlan {
source_present: source_present,
target_present: target_present,
edge_type_present: edge_type_present,
weight_default: 1.0,
};
}

// ---------------------------------------------------------------------------
// Original lines 84-110: `kg_query` — the query is required; the limit
// defaults to 10 and the mock entities cap at 3, each named Concept i
// of type concept.
// ---------------------------------------------------------------------------

pub struct KgQueryPlan {
query_present: bool,
limit_default: u64,
result_cap: u64,
}

pub fn kg_query(query_present: bool, limit: u64) -> KgQueryPlan {
return KgQueryPlan {
query_present: query_present,
limit_default: 10,
result_cap: capped_count(limit, 3),
};
}

// ---------------------------------------------------------------------------
// Original lines 113-138: `kg_traverse` — the source is required;
// max_depth defaults to 1 and the mock paths cap at 3, each of edge
// type related_to at depth i+1.
// ---------------------------------------------------------------------------

pub struct KgTraversePlan {
source_present: bool,
max_depth_default: u64,
path_cap: u64,
path_edge_type: str,
}

pub fn kg_traverse(source_present: bool, max_depth: u64) -> KgTraversePlan {
return KgTraversePlan {
source_present: source_present,
max_depth_default: 1,
path_cap: capped_count(max_depth, 3),
path_edge_type: "related_to",
};
}

// The shared cap rule: the mock enumerations never exceed three rows
// (original lines 96 and 125).
pub fn capped_count(limit: u64, cap: u64) -> u64 {
if (limit > cap) {
return cap;
}
return limit;
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original.
// ---------------------------------------------------------------------------

test cap_rule_direct {
assert(capped_count(10, 3) == 3);
assert(capped_count(2, 3) == 2);
assert(capped_count(3, 3) == 3);
}

test hash_stub_known_values {
assert(md5_compute("") == 5381);
const h = md5_compute("a");
assert(h == 5381 * 33 + 97);
assert(entity_hash_input("abc") == md5_compute("abc"));
}

test edge_hash_covers_all_three_parts {
assert(edge_hash_input("s", "t", "e") == md5_compute("s") + md5_compute("t") + md5_compute("e"));
}

test dispatch_table {
assert(dispatch("kg_create_entity"));
assert(dispatch("kg_create_edge"));
assert(dispatch("kg_query"));
assert(dispatch("kg_traverse"));
assert(!dispatch("kg_unknown"));
assert(dispatched_tool_count() == 4);
}

test create_entity_gate_and_defaults {
const ok = kg_create_entity(true, true);
assert(ok.entity_type_present);
assert(ok.name_present);
assert(ok.properties_default);
assert(ok.id_hashes_name_only);
}

test create_edge_gate_and_weight_default {
const ok = kg_create_edge(true, true, true);
assert(ok.source_present);
assert(ok.target_present);
assert(ok.edge_type_present);
assert(ok.weight_default == 1.0);
}

test query_gate_limit_and_cap {
const ok = kg_query(true, 10);
assert(ok.query_present);
assert(ok.limit_default == 10);
assert(ok.result_cap == 3);
const small = kg_query(true, 2);
assert(small.result_cap == 2);
}

test traverse_gate_depth_and_cap {
const ok = kg_traverse(true, 1);
assert(ok.source_present);
assert(ok.max_depth_default == 1);
assert(ok.path_cap == 1);
assert(ok.path_edge_type == "related_to");
const deep = kg_traverse(true, 9);
assert(deep.path_cap == 3);
}
Loading