From e1cae960dc7ebd6e0b46287fd00e51a07a6d9ecb Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sat, 17 Oct 2026 04:36:14 -0300 Subject: [PATCH] spec(port): port specs/port/trios/crates/trios-server/src/tools/trios_kg.t27 (Closes #9402) Port gHashTag/trios:crates/trios-server/src/tools/trios_kg.rs (at 8b229e9489ee, 148 lines) as specs/port/trios/crates/trios-server/ src/tools/trios_kg.t27 - Roadmap Stage 6, goal #4548, target t27c gen-rust. The trios-kg MCP tools' decisions are ported as pure functions and tested (8 tests, 8 pass, 0 vacuous, 26 runtime asserts, 100% function coverage): the four-name dispatch table, each tool's required-key gate with its defaults (entity_type and name for create_entity with the empty-object properties; source, target and edge_type for create_edge with the weight defaulting to 1.0; the query for kg_query with the limit defaulting to 10; the source for kg_traverse with max_depth defaulting to 1), the three-row cap on both mock enumerations, and the djb2-style hash stub - 5381, then hash = hash * 33 + byte per input byte - with the entity ids hashing the name alone and the edge ids covering all three parts. The JSON reading and building are the callers' plumbing. Verified: parse OK, spec-status IMPLEMENTED, typecheck 0 errors, coverage 10/10, test-report 8/8 pass, 0 BLOCKED, gen 141 lines with 0 'not yet implemented', boundary exactly this one file, dupe_scan: all 8 introduced names unique across 2040 specs AND all 4 introduced struct names absent from the corpus type set (both checked before the commit), bare body scan adds no duplicate group (752/221 unchanged). Dmitrii Fedorov ([@dmitrii-f-t27](https://github.com/dmitrii-f-t27)) --- .../trios-server/src/tools/trios_kg.t27 | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 specs/port/trios/crates/trios-server/src/tools/trios_kg.t27 diff --git a/specs/port/trios/crates/trios-server/src/tools/trios_kg.t27 b/specs/port/trios/crates/trios-server/src/tools/trios_kg.t27 new file mode 100644 index 0000000000..c935559796 --- /dev/null +++ b/specs/port/trios/crates/trios-server/src/tools/trios_kg.t27 @@ -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); +}