From 34229984c04d8171992b9ebaa386532514bf4ecf Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 31 May 2026 13:54:52 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(kg):=20add=20person=E2=86=92org=20and?= =?UTF-8?q?=20org=E2=86=92org=20edge=20endpoint=20pairs=20(#538=20#539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declares 7 new endpoint rules in KG_EDGE_RULES (pack-extensible, additive only): person→org part_of/instance_of, and org→org depends_on/enables/ contains/part_of/precedes. Updates valid_relations_for_entity_pair error enrichment, ADR-002 endpoint contract table, and adds 3 integration tests. Co-Authored-By: Claude Opus 4.6 --- crates/khive-pack-kg/src/handlers.rs | 8 ++ crates/khive-pack-kg/src/lib.rs | 48 +++++++- crates/khive-pack-kg/tests/integration.rs | 132 ++++++++++++++++++++++ docs/adr/ADR-002-edge-ontology.md | 24 ++++ 4 files changed, 211 insertions(+), 1 deletion(-) diff --git a/crates/khive-pack-kg/src/handlers.rs b/crates/khive-pack-kg/src/handlers.rs index 8d63a9e8..c0119f21 100644 --- a/crates/khive-pack-kg/src/handlers.rs +++ b/crates/khive-pack-kg/src/handlers.rs @@ -779,6 +779,14 @@ pub(crate) fn valid_relations_for_entity_pair(src_kind: &str, tgt_kind: &str) -> ("artifact", "supersedes", "artifact"), ("service", "supersedes", "service"), ("dataset", "supersedes", "dataset"), + // KG pack extensions (added v0.2.4): person→org and org→org pairs. + ("person", "part_of", "org"), + ("person", "instance_of", "org"), + ("org", "depends_on", "org"), + ("org", "enables", "org"), + ("org", "contains", "org"), + ("org", "part_of", "org"), + ("org", "precedes", "org"), ]; let mut relations: Vec<&'static str> = RULES .iter() diff --git a/crates/khive-pack-kg/src/lib.rs b/crates/khive-pack-kg/src/lib.rs index 9c60ee01..16bda13f 100644 --- a/crates/khive-pack-kg/src/lib.rs +++ b/crates/khive-pack-kg/src/lib.rs @@ -27,12 +27,53 @@ use serde_json::Value; use khive_runtime::pack::PackRuntime; use khive_runtime::{KhiveRuntime, NamespaceToken, RuntimeError, VerbRegistry}; -use khive_types::{HandlerDef, Pack, ParamDef, VerbCategory, Visibility}; +use khive_types::{EdgeEndpointRule, EdgeRelation, EndpointKind, HandlerDef, Pack, ParamDef, VerbCategory, Visibility}; pub use entity_type_registry::{EntityTypeDef, EntityTypeRegistry, ResolvedType}; pub use khive_types::EntityKind; pub use vocab::NoteKind; +/// ADR-002 §"Pack-extensible edge endpoints": KG pack extends the base entity→entity +/// allowlist with person→org and org→org relationship pairs. These are additive only +/// — the base contract in operations.rs is unchanged. +static KG_EDGE_RULES: [EdgeEndpointRule; 7] = [ + EdgeEndpointRule { + relation: EdgeRelation::PartOf, + source: EndpointKind::EntityOfKind("person"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::InstanceOf, + source: EndpointKind::EntityOfKind("person"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::DependsOn, + source: EndpointKind::EntityOfKind("org"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::Enables, + source: EndpointKind::EntityOfKind("org"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::Contains, + source: EndpointKind::EntityOfKind("org"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::PartOf, + source: EndpointKind::EntityOfKind("org"), + target: EndpointKind::EntityOfKind("org"), + }, + EdgeEndpointRule { + relation: EdgeRelation::Precedes, + source: EndpointKind::EntityOfKind("org"), + target: EndpointKind::EntityOfKind("org"), + }, +]; + /// KG pack vocabulary declaration. pub struct KgPack { runtime: KhiveRuntime, @@ -51,6 +92,7 @@ impl Pack for KgPack { "concept", "document", "dataset", "project", "person", "org", "artifact", "service", ]; const HANDLERS: &'static [HandlerDef] = &KG_HANDLERS; + const EDGE_RULES: &'static [EdgeEndpointRule] = &KG_EDGE_RULES; } // ADR-060 / ADR-025: Illocutionary classification (Searle 1976) @@ -813,6 +855,10 @@ impl PackRuntime for KgPack { &KG_HANDLERS } + fn edge_rules(&self) -> &'static [EdgeEndpointRule] { + ::EDGE_RULES + } + async fn warm(&self) { let _ = self.runtime.embed("khive warmup").await; } diff --git a/crates/khive-pack-kg/tests/integration.rs b/crates/khive-pack-kg/tests/integration.rs index c479a943..0774a88d 100644 --- a/crates/khive-pack-kg/tests/integration.rs +++ b/crates/khive-pack-kg/tests/integration.rs @@ -5157,3 +5157,135 @@ async fn withdraw_cas_divergence_after_approval() { "CAS divergence: proposal must still be 'applied' after failed withdraw; items: {list}" ); } + +// ---- KG pack edge endpoint extensions (ADR-002 v0.2.4) ---- +// +// These tests verify the 7 new endpoint pairs declared in KG_EDGE_RULES. +// Each test constructs a fixture with edge rules installed (mirroring what the +// MCP transport does at startup per ADR-031) before calling link(). + +fn pack_with_edge_rules() -> (Fixture, KhiveRuntime) { + let rt = KhiveRuntime::memory().expect("in-memory runtime must succeed"); + let mut builder = VerbRegistryBuilder::new(); + builder.register(KgPack::new(rt.clone())); + let registry = builder.build().expect("registry builds"); + rt.install_edge_rules(registry.all_edge_rules()); + (Fixture { registry }, rt) +} + +/// person→org with part_of must succeed after edge rules are installed. +#[tokio::test] +async fn link_person_to_org_part_of_succeeds() { + let (f, _rt) = pack_with_edge_rules(); + + let person = f + .dispatch( + "create", + json!({ "kind": "person", "name": "Alice Researcher" }), + ) + .await + .expect("create person"); + let org = f + .dispatch( + "create", + json!({ "kind": "org", "name": "DeepMind" }), + ) + .await + .expect("create org"); + + let result = f + .dispatch( + "link", + json!({ + "source_id": person["id"], + "target_id": org["id"], + "relation": "part_of", + }), + ) + .await; + + assert!( + result.is_ok(), + "person→org part_of must succeed with KG edge rules installed; got: {result:?}" + ); + let edge = result.unwrap(); + assert_eq!(edge["relation"], "part_of"); +} + +/// org→org with depends_on must succeed after edge rules are installed. +#[tokio::test] +async fn link_org_to_org_depends_on_succeeds() { + let (f, _rt) = pack_with_edge_rules(); + + let org_a = f + .dispatch( + "create", + json!({ "kind": "org", "name": "SubsidiaryInc" }), + ) + .await + .expect("create org_a"); + let org_b = f + .dispatch( + "create", + json!({ "kind": "org", "name": "ParentCorp" }), + ) + .await + .expect("create org_b"); + + let result = f + .dispatch( + "link", + json!({ + "source_id": org_a["id"], + "target_id": org_b["id"], + "relation": "depends_on", + }), + ) + .await; + + assert!( + result.is_ok(), + "org→org depends_on must succeed with KG edge rules installed; got: {result:?}" + ); + let edge = result.unwrap(); + assert_eq!(edge["relation"], "depends_on"); +} + +/// Regression: concept→concept extends must still work after adding KG edge rules. +#[tokio::test] +async fn link_concept_to_concept_extends_still_works() { + let (f, _rt) = pack_with_edge_rules(); + + let parent = f + .dispatch( + "create", + json!({ "kind": "concept", "name": "Attention" }), + ) + .await + .expect("create parent concept"); + let child = f + .dispatch( + "create", + json!({ "kind": "concept", "name": "FlashAttention" }), + ) + .await + .expect("create child concept"); + + let result = f + .dispatch( + "link", + json!({ + "source_id": child["id"], + "target_id": parent["id"], + "relation": "extends", + }), + ) + .await; + + assert!( + result.is_ok(), + "concept→concept extends must still succeed (regression); got: {result:?}" + ); + let edge = result.unwrap(); + assert_eq!(edge["relation"], "extends"); +} diff --git a/docs/adr/ADR-002-edge-ontology.md b/docs/adr/ADR-002-edge-ontology.md index a5ea0b1d..bd0d8018 100644 --- a/docs/adr/ADR-002-edge-ontology.md +++ b/docs/adr/ADR-002-edge-ontology.md @@ -256,6 +256,30 @@ Those are better modeled with `extends`, `variant_of`, `supersedes`, or metadata `annotates` is the only relation that crosses substrate kinds. Source is always a note. Target may be any existing UUID (entity, note, event, edge) in the caller's namespace. +#### KG pack extensions (added v0.2.4) + +The KG pack extends the base endpoint contract via `EDGE_RULES` to cover +person→org and org→org relationships common in research KGs: + +| Source | Relation | Target | Added | +| -------- | ------------ | ------ | ------- | +| `Person` | `part_of` | `Org` | v0.2.4 | +| `Person` | `instance_of`| `Org` | v0.2.4 | +| `Org` | `depends_on` | `Org` | v0.2.4 | +| `Org` | `enables` | `Org` | v0.2.4 | +| `Org` | `contains` | `Org` | v0.2.4 | +| `Org` | `part_of` | `Org` | v0.2.4 | +| `Org` | `precedes` | `Org` | v0.2.4 | + +These are additive — the base contract is unchanged. Semantics: +- `Person part_of Org` — a person is a member or employee of an org +- `Person instance_of Org` — a person represents or embodies an org (e.g. a founder) +- `Org depends_on Org` — one org depends on another (e.g. subsidiary dependency) +- `Org enables Org` — one org enables another (e.g. incubator → startup) +- `Org contains Org` — org hierarchy (e.g. parent company contains subsidiary) +- `Org part_of Org` — inverse of contains; subsidiary is part of parent +- `Org precedes Org` — temporal ordering without replacement (predecessor org) + ## Edge Metadata `Edge.metadata` remains open JSON for relation-specific annotations. ADR-governed metadata From f5bc6c4bed8633408f6593bce75f440ef5414bdf Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 31 May 2026 13:55:15 -0400 Subject: [PATCH 2/3] style: deno fmt ADR-002 --- docs/adr/ADR-002-edge-ontology.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/adr/ADR-002-edge-ontology.md b/docs/adr/ADR-002-edge-ontology.md index bd0d8018..de205a59 100644 --- a/docs/adr/ADR-002-edge-ontology.md +++ b/docs/adr/ADR-002-edge-ontology.md @@ -261,17 +261,18 @@ Target may be any existing UUID (entity, note, event, edge) in the caller's name The KG pack extends the base endpoint contract via `EDGE_RULES` to cover person→org and org→org relationships common in research KGs: -| Source | Relation | Target | Added | -| -------- | ------------ | ------ | ------- | -| `Person` | `part_of` | `Org` | v0.2.4 | -| `Person` | `instance_of`| `Org` | v0.2.4 | -| `Org` | `depends_on` | `Org` | v0.2.4 | -| `Org` | `enables` | `Org` | v0.2.4 | -| `Org` | `contains` | `Org` | v0.2.4 | -| `Org` | `part_of` | `Org` | v0.2.4 | -| `Org` | `precedes` | `Org` | v0.2.4 | +| Source | Relation | Target | Added | +| -------- | ------------- | ------ | ------ | +| `Person` | `part_of` | `Org` | v0.2.4 | +| `Person` | `instance_of` | `Org` | v0.2.4 | +| `Org` | `depends_on` | `Org` | v0.2.4 | +| `Org` | `enables` | `Org` | v0.2.4 | +| `Org` | `contains` | `Org` | v0.2.4 | +| `Org` | `part_of` | `Org` | v0.2.4 | +| `Org` | `precedes` | `Org` | v0.2.4 | These are additive — the base contract is unchanged. Semantics: + - `Person part_of Org` — a person is a member or employee of an org - `Person instance_of Org` — a person represents or embodies an org (e.g. a founder) - `Org depends_on Org` — one org depends on another (e.g. subsidiary dependency) From 0e65166c4be401032b6c3de72f4ba72f76536466 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 31 May 2026 14:01:08 -0400 Subject: [PATCH 3/3] style: cargo fmt integration tests --- crates/khive-pack-kg/src/lib.rs | 5 ++++- crates/khive-pack-kg/tests/integration.rs | 20 ++++---------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/crates/khive-pack-kg/src/lib.rs b/crates/khive-pack-kg/src/lib.rs index 16bda13f..2684a29a 100644 --- a/crates/khive-pack-kg/src/lib.rs +++ b/crates/khive-pack-kg/src/lib.rs @@ -27,7 +27,10 @@ use serde_json::Value; use khive_runtime::pack::PackRuntime; use khive_runtime::{KhiveRuntime, NamespaceToken, RuntimeError, VerbRegistry}; -use khive_types::{EdgeEndpointRule, EdgeRelation, EndpointKind, HandlerDef, Pack, ParamDef, VerbCategory, Visibility}; +use khive_types::{ + EdgeEndpointRule, EdgeRelation, EndpointKind, HandlerDef, Pack, ParamDef, VerbCategory, + Visibility, +}; pub use entity_type_registry::{EntityTypeDef, EntityTypeRegistry, ResolvedType}; pub use khive_types::EntityKind; diff --git a/crates/khive-pack-kg/tests/integration.rs b/crates/khive-pack-kg/tests/integration.rs index 0774a88d..af70bdd7 100644 --- a/crates/khive-pack-kg/tests/integration.rs +++ b/crates/khive-pack-kg/tests/integration.rs @@ -5186,10 +5186,7 @@ async fn link_person_to_org_part_of_succeeds() { .await .expect("create person"); let org = f - .dispatch( - "create", - json!({ "kind": "org", "name": "DeepMind" }), - ) + .dispatch("create", json!({ "kind": "org", "name": "DeepMind" })) .await .expect("create org"); @@ -5218,17 +5215,11 @@ async fn link_org_to_org_depends_on_succeeds() { let (f, _rt) = pack_with_edge_rules(); let org_a = f - .dispatch( - "create", - json!({ "kind": "org", "name": "SubsidiaryInc" }), - ) + .dispatch("create", json!({ "kind": "org", "name": "SubsidiaryInc" })) .await .expect("create org_a"); let org_b = f - .dispatch( - "create", - json!({ "kind": "org", "name": "ParentCorp" }), - ) + .dispatch("create", json!({ "kind": "org", "name": "ParentCorp" })) .await .expect("create org_b"); @@ -5257,10 +5248,7 @@ async fn link_concept_to_concept_extends_still_works() { let (f, _rt) = pack_with_edge_rules(); let parent = f - .dispatch( - "create", - json!({ "kind": "concept", "name": "Attention" }), - ) + .dispatch("create", json!({ "kind": "concept", "name": "Attention" })) .await .expect("create parent concept"); let child = f