khive_types/pack.rs
1//! Pack trait — the declarative composition unit for khive (ADR-025).
2//!
3//! A pack declares vocabulary (note kinds, entity kinds), verbs, and edge
4//! endpoint rules. This is purely static metadata — no I/O, no async.
5//! Runtime dispatch lives in `khive-runtime` (`PackRuntime` trait +
6//! `VerbRegistry`).
7//!
8//! This trait lives in khive-types (no_std, zero deps) so downstream crates
9//! can reference pack metadata without pulling in the full runtime.
10
11use crate::edge::EdgeRelation;
12
13/// Visibility tier for a handler (ADR-023).
14///
15/// `Verb` entries appear on the MCP wire and are invokable by agents.
16/// `Subhandler` entries are internal — callable by the operator via CLI
17/// but not surfaced as top-level MCP verbs.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Visibility {
20 /// Externally invokable via MCP `request` tool.
21 Verb,
22 /// Internal — operator-only via `kkernel call <pack> <handler>`.
23 Subhandler,
24}
25
26/// Illocutionary force classification for a verb handler (ADR-025).
27///
28/// Follows Searle's five speech-act categories (1976). Every `Visibility::Verb`
29/// handler in the MCP surface MUST carry a category. `Subhandler` entries may
30/// use the category of their parent verb or `Assertive` as a sensible default.
31///
32/// The category is a documentation / introspection tag. It is NOT used for
33/// permission checking, transport routing, or return-shape selection (ADR-025 §4).
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum VerbCategory {
36 /// Speaker represents a state of affairs — retrieves and presents facts.
37 /// Examples: `get`, `list`, `search`, `recall`.
38 Assertive,
39 /// Speaker attempts to get the hearer to do something.
40 /// Examples: `assign`, `transition`.
41 Directive,
42 /// Speaker commits to a persistent change.
43 /// Examples: `create`, `remember`, `link`, `send`.
44 Commissive,
45 /// Speaker changes institutional status by fiat.
46 /// Examples: `update`, `delete`, `merge`, `complete`.
47 Declaration,
48 // `Expressive` is intentionally absent — no verb currently uses it (ADR-025 §Why expressive stays empty).
49}
50
51/// Handler metadata for discovery and documentation (ADR-023, ADR-025).
52///
53/// Replaces the previous `VerbDef`. Every entry carries a `visibility` tag
54/// so the registry can separate the MCP-exposed surface from internal handlers,
55/// and a `category` that classifies the illocutionary force of the verb
56/// per the speech-act taxonomy in ADR-025.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct HandlerDef {
59 pub name: &'static str,
60 pub description: &'static str,
61 pub visibility: Visibility,
62 /// Illocutionary force classification (ADR-025). Use `Assertive` for
63 /// `Subhandler` entries that have no external callers.
64 pub category: VerbCategory,
65}
66
67/// Backward-compatible type alias. Existing code that names `VerbDef` still
68/// compiles; new code should use `HandlerDef` directly (ADR-023).
69#[deprecated(since = "0.2.0", note = "Use HandlerDef instead (ADR-023)")]
70pub type VerbDef = HandlerDef;
71
72/// Match spec for one end of an [`EdgeEndpointRule`] (ADR-031).
73///
74/// Identifies a substrate + kind pair that the rule applies to. Note that
75/// `kind` strings refer to the pack-declared note kinds / entity kinds — not
76/// the closed [`EdgeRelation`] set, which is universal.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum EndpointKind {
79 /// A note whose `kind` field equals the given string (e.g. `"task"`).
80 NoteOfKind(&'static str),
81 /// An entity whose `kind` field equals the given string (e.g. `"concept"`).
82 EntityOfKind(&'static str),
83}
84
85/// A pack-declared endpoint rule for a specific edge relation (ADR-031).
86///
87/// Rules are **additive**: they extend the set of allowed
88/// `(source, relation, target)` triples beyond the ADR-002 base contract.
89/// Packs cannot tighten the base rules — only broaden them. The closed
90/// [`EdgeRelation`] taxonomy itself is not extended; only the endpoint
91/// contract per relation is.
92///
93/// Example — GTD pack allows `depends_on` between task notes:
94///
95/// ```ignore
96/// EdgeEndpointRule {
97/// relation: EdgeRelation::DependsOn,
98/// source: EndpointKind::NoteOfKind("task"),
99/// target: EndpointKind::NoteOfKind("task"),
100/// }
101/// ```
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub struct EdgeEndpointRule {
104 pub relation: EdgeRelation,
105 pub source: EndpointKind,
106 pub target: EndpointKind,
107}
108
109/// Lifecycle specification for a note kind (ADR-004 §NoteKindSpec).
110///
111/// Declares which field holds the kind's domain state, the initial value,
112/// terminal values, and allowed transitions. The runtime uses this to
113/// validate lifecycle operations at the verb boundary without hard-coding
114/// kind-specific logic in the shared CRUD path.
115///
116/// Phase 1 (current): packs declare the spec; the runtime records it for
117/// documentation and future enforcement.
118/// Phase 2 (future ADR): the runtime uses `field` to route lifecycle writes
119/// to a first-class column rather than `properties`.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct NoteLifecycleSpec {
122 /// The field name that holds the kind's lifecycle state.
123 ///
124 /// ADR-004 mandates `"kind_status"` for pack-owned lifecycle fields to
125 /// avoid the semantic collision with `Note.status` (NoteStatus).
126 pub field: &'static str,
127 /// The value assigned when a note of this kind is first created.
128 pub initial: &'static str,
129 /// Values from which no further transitions are possible.
130 pub terminal: &'static [&'static str],
131 /// Allowed `(from, to)` transitions. `"*"` as `from` matches any state.
132 pub transitions: &'static [(&'static str, &'static str)],
133}
134
135/// Kind-level schema specification for a note kind (ADR-004 §NoteKindSpec).
136///
137/// Each pack-registered note kind may declare a `NoteKindSpec` to describe
138/// its lifecycle semantics. The runtime collects these at boot time via
139/// [`Pack::NOTE_KIND_SPECS`] for documentation, introspection, and (in future
140/// ADRs) enforcement.
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct NoteKindSpec {
143 /// The note kind string this spec governs (e.g. `"task"`).
144 pub kind: &'static str,
145 /// Alternate names this kind accepts on the wire.
146 pub aliases: &'static [&'static str],
147 /// Lifecycle state machine for this kind.
148 pub lifecycle: NoteLifecycleSpec,
149}
150
151/// DDL statements the pack needs applied to the auxiliary schema (ADR-019).
152///
153/// Pack-auxiliary tables use idempotent `CREATE TABLE IF NOT EXISTS`; they are
154/// not part of the core versioned migration chain. The runtime applies these
155/// statements once at pack registration time (or startup) against the active
156/// storage backend.
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct PackSchemaPlan {
159 /// The pack this schema plan belongs to (used for error reporting).
160 pub pack: &'static str,
161 /// Idempotent SQL statements to apply.
162 pub statements: &'static [&'static str],
163}
164
165/// A composable module that contributes vocabulary, verbs, and edge endpoint
166/// rules to the khive runtime.
167///
168/// Packs declare what entity kinds, note kinds, and verbs they introduce, and
169/// optionally extend the per-relation endpoint contract via [`EDGE_RULES`].
170/// The runtime merges vocabularies from all loaded packs and rejects
171/// unregistered kinds at the service boundary.
172///
173/// The closed [`EdgeRelation`] enum (ADR-021) is not extensible — only its
174/// per-relation endpoint contract is (ADR-031).
175///
176/// [`EDGE_RULES`]: Pack::EDGE_RULES
177pub trait Pack {
178 /// Short identifier for this pack (e.g. "kg", "tasks").
179 const NAME: &'static str;
180
181 /// Note kinds this pack contributes to the runtime vocabulary.
182 const NOTE_KINDS: &'static [&'static str];
183
184 /// Entity kinds this pack contributes to the runtime vocabulary.
185 const ENTITY_KINDS: &'static [&'static str];
186
187 /// Handlers this pack registers (ADR-023).
188 ///
189 /// The runtime routes verb calls to the pack that declares them.
190 /// Only entries with `visibility: Visibility::Verb` are surfaced on the
191 /// MCP wire; `Visibility::Subhandler` entries are internal.
192 const HANDLERS: &'static [HandlerDef];
193
194 /// Additional edge endpoint rules this pack contributes (ADR-031).
195 ///
196 /// Defaults to empty — packs that introduce no new endpoint pairs (or
197 /// only rely on the ADR-002 base contract) can ignore this.
198 const EDGE_RULES: &'static [EdgeEndpointRule] = &[];
199
200 /// Other pack names whose vocabulary this pack references (ADR-037).
201 ///
202 /// The runtime checks that every name in `REQUIRES` appears in the
203 /// loaded pack set before any pack is registered. Defaults to empty
204 /// so existing packs compile without changes.
205 const REQUIRES: &'static [&'static str] = &[];
206
207 /// Lifecycle and schema specs for note kinds this pack owns (ADR-004).
208 ///
209 /// Packs that introduce note kinds with explicit lifecycle semantics
210 /// (e.g. GTD's `task` kind) declare the spec here. The runtime collects
211 /// these at boot time for introspection and future enforcement. Defaults
212 /// to empty so existing packs compile without changes.
213 const NOTE_KIND_SPECS: &'static [NoteKindSpec] = &[];
214
215 /// Pack-auxiliary schema plan (ADR-019).
216 ///
217 /// Packs that need their own auxiliary tables (e.g. GTD's
218 /// `gtd_lifecycle_audit`) declare idempotent DDL statements here.
219 /// The runtime applies them once at registration time. Defaults to
220 /// `None` so packs with no auxiliary schema cost nothing.
221 const SCHEMA_PLAN: Option<PackSchemaPlan> = None;
222
223 /// Validation rule IDs contributed by this pack (ADR-034).
224 ///
225 /// Rule IDs are namespaced by pack name: `<pack-name>/<rule-id>`.
226 /// The runtime merges rule IDs from all packs; the actual rule
227 /// implementations live in `khive-runtime::validation::ValidationRule`
228 /// (not in `khive-types`, which stays `no_std`). This const serves as
229 /// the declarative catalog of rule identifiers so the validation
230 /// infrastructure can enumerate what rules a pack claims without
231 /// loading the runtime.
232 ///
233 /// Defaults to empty — packs with no domain-specific validation rules
234 /// can leave this unset.
235 const VALIDATION_RULES: &'static [&'static str] = &[];
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 struct TestPack;
243
244 impl Pack for TestPack {
245 const NAME: &'static str = "test";
246 const NOTE_KINDS: &'static [&'static str] = &["memo"];
247 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
248 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
249 name: "do_thing",
250 description: "does a thing",
251 visibility: Visibility::Verb,
252 category: VerbCategory::Commissive,
253 }];
254 }
255
256 #[test]
257 fn pack_trait_compiles() {
258 assert_eq!(TestPack::NAME, "test");
259 assert_eq!(TestPack::NOTE_KINDS, &["memo"]);
260 assert_eq!(TestPack::ENTITY_KINDS, &["widget"]);
261 assert_eq!(TestPack::HANDLERS.len(), 1);
262 assert_eq!(TestPack::HANDLERS[0].name, "do_thing");
263 assert_eq!(TestPack::HANDLERS[0].visibility, Visibility::Verb);
264 assert_eq!(TestPack::HANDLERS[0].category, VerbCategory::Commissive);
265 }
266
267 #[test]
268 fn verb_category_variants_exist() {
269 // Just ensuring the enum variants are accessible — no runtime assertion
270 // needed beyond confirming they exist at compile time.
271 let _ = VerbCategory::Assertive;
272 let _ = VerbCategory::Directive;
273 let _ = VerbCategory::Commissive;
274 let _ = VerbCategory::Declaration;
275 }
276
277 #[test]
278 fn pack_validation_rules_default_empty() {
279 assert!(TestPack::VALIDATION_RULES.is_empty());
280 }
281}