Skip to content
Open
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions crates/khive-pack-kg/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
51 changes: 50 additions & 1 deletion crates/khive-pack-kg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,56 @@ 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,
Expand All @@ -51,6 +95,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)
Expand Down Expand Up @@ -813,6 +858,10 @@ impl PackRuntime for KgPack {
&KG_HANDLERS
}

fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
<KgPack as Pack>::EDGE_RULES
}

async fn warm(&self) {
let _ = self.runtime.embed("khive warmup").await;
}
Expand Down
120 changes: 120 additions & 0 deletions crates/khive-pack-kg/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5157,3 +5157,123 @@ 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");
}
25 changes: 25 additions & 0 deletions docs/adr/ADR-002-edge-ontology.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,31 @@ 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
Expand Down
Loading