khive_runtime/pack/traits.rs
1use std::any::Any;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use khive_storage::EventView;
6use khive_types::NotePropertyPolicySpec;
7use serde_json::Value;
8
9use crate::context::ContextContributor;
10use crate::operations::LinkSpec;
11use crate::runtime::NamespaceToken;
12use crate::validation::ValidationRule;
13use crate::{KhiveRuntime, RuntimeError};
14
15use super::{
16 ChannelIngestCapability, EdgeEndpointRule, EntityTypeDef, HandlerDef, NoteEmbeddingPolicySpec,
17 NoteKindSpec, PackColumnAddition, VerbRegistry,
18};
19#[cfg(doc)]
20use super::{PackFactory, PackRegistry};
21
22/// Pack-auxiliary schema plan.
23///
24/// Declares `CREATE TABLE IF NOT EXISTS` statements for pack-owned tables that
25/// are NOT part of the core substrate schema (entities, notes, edges, events).
26/// Applied at boot via `StorageBackend::apply_pack_ddl_statements_with_columns`,
27/// together with [`PackRuntime::schema_column_additions`].
28///
29/// Core substrate tables evolve through versioned migrations. Pack schema is
30/// strictly for pack-auxiliary tables (e.g. GTD lifecycle audit, memory index).
31/// v1 pack schemas are non-versioned.
32#[derive(Debug, Default, Clone)]
33pub struct SchemaPlan {
34 /// Owning pack name.
35 pub pack: &'static str,
36 /// DDL statements applied idempotently at boot.
37 /// Each entry must be a self-contained `CREATE TABLE IF NOT EXISTS` or
38 /// similar idempotent statement.
39 pub statements: &'static [&'static str],
40}
41
42impl SchemaPlan {
43 /// Construct a `SchemaPlan` with no statements.
44 ///
45 /// Packs whose state lives entirely in the core substrate tables (entities,
46 /// notes, edges) use this as their `schema_plan()` return value.
47 pub const fn empty() -> Self {
48 Self {
49 pack: "",
50 statements: &[],
51 }
52 }
53
54 /// Returns `true` when the plan contains no DDL statements.
55 pub fn is_empty(&self) -> bool {
56 self.statements.is_empty()
57 }
58}
59
60/// Best-effort hook called after every successful verb dispatch.
61///
62/// The runtime supplies a synthetic [`EventView`] whose `event` describes the
63/// dispatch outcome and whose `observations` vector is currently empty. Loading
64/// persisted provenance observations belongs to an explicit caller or the
65/// deferred event-consumer contract; this hook does not provide it.
66#[async_trait]
67pub trait DispatchHook: Send + Sync {
68 /// Called with the dispatch-outcome event view after a successful pack dispatch.
69 ///
70 /// Errors are logged via `tracing::warn!` and never propagated to the
71 /// caller; the dispatch has already succeeded.
72 async fn on_dispatch(&self, view: &EventView);
73}
74
75/// Async dispatch trait for packs.
76///
77/// This is the object-safe behavioral counterpart to `khive_types::Pack`.
78/// `Pack` uses const associated items (not object-safe in Rust); this trait
79/// mirrors that metadata as methods and adds async dispatch.
80///
81/// Registration requires `P: Pack + PackRuntime` — the compiler enforces
82/// that every runtime pack also declares its vocabulary via `Pack`.
83#[async_trait]
84pub trait PackRuntime: Send + Sync {
85 /// Pack name — must equal `<Self as Pack>::NAME`.
86 fn name(&self) -> &str;
87
88 /// Optional instance-owned state for host work outside verb dispatch.
89 /// Return the same shared state used by this pack's handlers. The host
90 /// owns task startup and shutdown; this accessor must not start work.
91 fn host_state(&self) -> Option<Arc<dyn Any + Send + Sync>> {
92 None
93 }
94
95 /// Validate this pack instance's configuration before it can execute.
96 /// Metadata-only construction does not activate packs.
97 fn validate_config(&self) -> Result<(), RuntimeError> {
98 Ok(())
99 }
100
101 /// Note kinds this pack owns — must equal `<Self as Pack>::NOTE_KINDS`.
102 fn note_kinds(&self) -> &'static [&'static str];
103
104 /// Entity kinds this pack owns — must equal `<Self as Pack>::ENTITY_KINDS`.
105 fn entity_kinds(&self) -> &'static [&'static str];
106
107 /// Brain profile consumer kinds this pack requests — must equal
108 /// `<Self as Pack>::BRAIN_CONSUMER_KINDS`.
109 fn brain_consumer_kinds(&self) -> &'static [&'static str] {
110 &[]
111 }
112
113 /// Trusted in-process section feedback after the calling pack has validated
114 /// its own target. This is not a registered handler or a wire entry point.
115 async fn apply_profile_section_feedback(
116 &self,
117 _token: &NamespaceToken,
118 _profile_id: &str,
119 _section_signals: Value,
120 _target_attribution: Option<String>,
121 ) -> Result<Value, RuntimeError> {
122 Err(RuntimeError::InvalidInput(format!(
123 "pack {:?} does not support profile section feedback",
124 self.name()
125 )))
126 }
127
128 /// Handlers this pack registers — must equal `<Self as Pack>::HANDLERS`.
129 fn handlers(&self) -> &'static [HandlerDef];
130
131 /// Optional canonical input schema owned by the pack; ParamDefs remain available.
132 fn input_schema(&self, _verb: &str) -> Option<Value> {
133 None
134 }
135
136 /// Pack-extensible edge endpoint rules — must equal `<Self as Pack>::EDGE_RULES`.
137 /// Defaults to empty so existing packs that don't extend the edge contract
138 /// can ignore it.
139 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
140 &[]
141 }
142
143 /// Pack-extensible entity-type subtypes — must equal `<Self as Pack>::ENTITY_TYPES`.
144 /// Defaults to empty so existing packs that don't extend the entity_type
145 /// registry can ignore it.
146 fn entity_types(&self) -> &'static [EntityTypeDef] {
147 &[]
148 }
149
150 /// Pack names whose vocabulary this pack references.
151 /// Defaults to empty so existing packs compile without changes.
152 fn requires(&self) -> &'static [&'static str] {
153 &[]
154 }
155
156 /// NoteKindSpec declarations for note kinds this pack owns.
157 ///
158 /// Packs that introduce note kinds with explicit lifecycle semantics
159 /// declare the spec here. The runtime collects these for introspection
160 /// and future enforcement. Defaults to empty so existing packs compile
161 /// without changes.
162 fn note_kind_specs(&self) -> &'static [NoteKindSpec] {
163 &[]
164 }
165
166 /// Property-policy metadata for this pack's own note kinds.
167 ///
168 /// Mirror `Pack::NOTE_PROPERTY_POLICIES`. Boot validates and snapshots this
169 /// slice once, before activation. It does not install write enforcement.
170 fn note_property_policies(&self) -> &'static [NotePropertyPolicySpec] {
171 &[]
172 }
173
174 /// Per-kind write-time embedding policy; unlisted kinds use every model.
175 fn note_embedding_policies(&self) -> &'static [NoteEmbeddingPolicySpec] {
176 &[]
177 }
178
179 /// Optional per-kind hook for shared CRUD specialization.
180 ///
181 /// When a kind is owned by this pack (declared in `note_kinds()` or
182 /// `entity_kinds()`), returning `Some(hook)` opts that kind into
183 /// pack-specific behavior — defaults, derived properties, side-effect
184 /// edges — through the shared `create` path. Returning `None` keeps
185 /// the kind as plain storage with no specialization.
186 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
187 None
188 }
189
190 /// Optional context source, discovered without invoking contribution or dispatch.
191 ///
192 /// Return the instance-owned capability. Packs without context support keep the default.
193 /// See `docs/api/pack.md#context_contributor` for ownership and score contracts.
194 fn context_contributor(&self) -> Option<Arc<dyn ContextContributor>> {
195 None
196 }
197
198 /// Accept the trusted channel-ingest capability grant for this specific
199 /// pack instance.
200 ///
201 /// Called at most once per instance, immediately after this instance is
202 /// constructed via [`PackFactory::create_install`], and only for packs
203 /// whose name appears in `CHANNEL_INGEST_CAPABLE_PACKS`. Storing the
204 /// grant on `self` (rather than on the `&'static dyn PackFactory`, which
205 /// is a single process-wide singleton shared by every instance the
206 /// factory ever creates) makes the grant instance-bound: a `CommPack`
207 /// built outside [`PackRegistry::register_packs`] holds no capability
208 /// unless something calls this on that specific instance. Defaults to a
209 /// no-op so packs outside the allowlist compile without changes.
210 fn accept_channel_ingest_capability(&self, _capability: ChannelIngestCapability) {}
211
212 /// Pack-auxiliary schema.
213 ///
214 /// Returns DDL statements for pack-owned tables that are NOT part of the
215 /// core substrate schema. Statements are idempotent (`CREATE TABLE IF NOT
216 /// EXISTS`) so callers can apply them safely on every registration. Core
217 /// substrate tables evolve through versioned migrations; pack schema is
218 /// strictly pack-auxiliary.
219 ///
220 /// Defaults to an empty plan — packs that store everything in the core
221 /// substrate tables (entities, notes, edges, events) return this default.
222 ///
223 /// Plans are aggregated via [`VerbRegistry::all_schema_plans`] and applied
224 /// at startup via `KhiveMcpServer::with_packs`. Packs that need their
225 /// schema present (e.g. GTD) also self-bootstrap lazily on first call for
226 /// robustness in test contexts that create fresh in-memory databases.
227 fn schema_plan(&self) -> SchemaPlan {
228 SchemaPlan::empty()
229 }
230
231 /// Nullable-column upgrades for this pack's auxiliary tables.
232 ///
233 /// Must equal `Pack::SCHEMA_COLUMN_ADDITIONS`. The backend validates and
234 /// adds missing columns on existing tables before applying the full schema
235 /// plan, then validates every declared column. Both steps share the plan's
236 /// transaction. Defaults to empty for packs with no auxiliary upgrades.
237 fn schema_column_additions(&self) -> &'static [PackColumnAddition] {
238 &[]
239 }
240
241 /// Domain-specific validation rules contributed by this pack.
242 ///
243 /// Rule IDs MUST follow the `<pack>/<rule-id>` namespace convention.
244 /// Built-in rules (no pack prefix) are reserved for the `khive-runtime`
245 /// validation infrastructure.
246 ///
247 /// Defaults to empty — packs with no domain-specific rules return `&[]`.
248 fn validation_rules(&self) -> &'static [ValidationRule] {
249 &[]
250 }
251
252 /// Register custom embedding providers with the runtime. Called during pack
253 /// initialisation, before the first verb dispatch, so `KhiveRuntime::embedder(name)`
254 /// resolves provider names declared here. Default no-op — packs that only use
255 /// built-in lattice models do not need to override this.
256 /// See `docs/api/pack.md#register_embedders` for a usage example.
257 fn register_embedders(&self, _runtime: &KhiveRuntime) {}
258
259 /// Install a pack-owned entity-type validator on the runtime, called during pack
260 /// initialisation (after the registry is built, before the first dispatch) so
261 /// `create_many`/`create_entity` reject unregistered `entity_type` values at the
262 /// runtime layer. Default no-op leaves the validator absent (skip-when-None).
263 /// See `docs/api/pack.md#register_entity_type_validator` for the two-hook compatibility contract.
264 fn register_entity_type_validator(&self, _runtime: &KhiveRuntime) {}
265
266 /// Install a pack-owned entity-type validator that also receives the boot-time
267 /// composed set of every loaded pack's `ENTITY_TYPES` ([`VerbRegistry::all_entity_types`]).
268 /// Defaults to calling [`register_entity_type_validator`](Self::register_entity_type_validator)
269 /// with just the runtime. `call_register_entity_type_validators` calls this hook, not
270 /// the simpler one — override this to receive the composed vocabulary.
271 /// See `docs/api/pack.md#register_entity_type_validator` for the two-hook compatibility contract.
272 fn register_entity_type_validator_with_types(
273 &self,
274 runtime: &KhiveRuntime,
275 _pack_entity_types: &[EntityTypeDef],
276 ) {
277 self.register_entity_type_validator(runtime);
278 }
279
280 /// Install a pack-owned note-mutation hook on the runtime, called during pack
281 /// initialisation with the same timing as `register_entity_type_validator`. Packs
282 /// that cache derived state keyed by note content (e.g. `khive-pack-memory`'s warm
283 /// ANN index) override this to install a hook via
284 /// `KhiveRuntime::install_note_mutation_hook`. Default no-op leaves the hook absent.
285 /// See `docs/api/pack.md#register_note_mutation_hook` for cross-pack notification rationale.
286 fn register_note_mutation_hook(&self, _runtime: &KhiveRuntime) {}
287
288 /// Install a backend-matched note-search ANN candidate source. The
289 /// memory pack supplies it after registration; packs without a matching
290 /// graph leave the runtime's exact vector-store route in place.
291 fn register_note_search_ann_provider(&self, _runtime: &KhiveRuntime) {}
292
293 /// Install a note-write validator on the runtime, called at pack
294 /// initialisation with the same timing as `register_note_mutation_hook`.
295 ///
296 /// A pack owning a note kind whose properties carry identity that the
297 /// runtime can derive from the authorization token implements this and
298 /// calls `KhiveRuntime::install_note_write_validator`, so the identity is
299 /// derived at every note-write site rather than trusted from caller input
300 /// on the write paths that reach no pack verb. Default no-op leaves the
301 /// slot absent. The slot holds one validator, so an implementation must
302 /// return properties for kinds it does not own unchanged.
303 fn register_note_write_validator(&self, _runtime: &KhiveRuntime) {}
304
305 /// Warm up any in-memory state from persisted snapshots (optional). Called after
306 /// all packs are registered but before serving the first request. Must be
307 /// idempotent and infallible — errors are logged internally, never propagated.
308 async fn warm(&self) {}
309
310 /// Names of all embedding models registered on this pack's underlying runtime
311 /// handle. Defaults to empty — only packs that own embedding-bearing verbs
312 /// (kg, memory) need to override this.
313 /// See `docs/api/pack.md#registered_embedding_model_names` for the ADR-103 consumer.
314 fn registered_embedding_model_names(&self) -> Vec<String> {
315 Vec::new()
316 }
317
318 fn mounted_namespace(&self) -> Option<&str> {
319 None
320 }
321
322 /// Advisory owned catalog only: no storage, process, gate, or audit work.
323 fn mounted_catalog_snapshot(&self) -> Vec<crate::mounted_verb::MountedVerb> {
324 Vec::new()
325 }
326
327 async fn mounted_catalog(&self) -> Result<Vec<crate::mounted_verb::MountedVerb>, RuntimeError> {
328 Ok(Vec::new())
329 }
330
331 async fn dispatch_mounted(
332 &self,
333 _definition: &crate::mounted_verb::MountedVerb,
334 verb: &str,
335 params: Value,
336 registry: &VerbRegistry,
337 token: &NamespaceToken,
338 ) -> Result<Value, RuntimeError> {
339 self.dispatch(verb, params, registry, token).await
340 }
341
342 /// Dispatch a verb call. Returns serialized JSON response.
343 ///
344 /// The `registry` parameter gives the handler access to the merged
345 /// vocabulary and kind hooks across all loaded packs.
346 /// The `token` is an authorized namespace token minted by the dispatch
347 /// boundary after gate authorization — handlers must use it directly.
348 async fn dispatch(
349 &self,
350 verb: &str,
351 params: Value,
352 registry: &VerbRegistry,
353 token: &NamespaceToken,
354 ) -> Result<Value, RuntimeError>;
355}
356
357/// Per-kind specialization for shared CRUD.
358///
359/// Packs implement `KindHook` for kinds they own that need:
360/// - **Defaults** filled into create args (e.g. `status="inbox"` for tasks)
361/// - **Derived properties** computed from args (e.g. salience from priority)
362/// - **Side-effect writes** after the storage commit (e.g. `depends_on` edges)
363/// - **Cross-pack validation** before shared CRUD mutates an owned kind
364///
365/// Hooks are stateless from the framework's perspective — they receive the
366/// runtime and the current mutation inputs as method parameters. The pack
367/// registers them via [`PackRuntime::kind_hook`].
368///
369/// Lifecycle verbs (e.g. gtd's `complete`, `transition`) remain pack-owned
370/// verbs. Shared `create`, note `update`, entity `update`, and `link` calls
371/// flow through this trait when an endpoint kind has an owning pack hook.
372///
373/// A hook that still overrides the removed sequencing method does not compile, which is the point
374/// of the move: an implementor cannot replace the validator by replacing the sequence, because
375/// there is no sequence on this trait to replace.
376///
377/// ```compile_fail
378/// use async_trait::async_trait;
379/// use khive_runtime::{KhiveRuntime, KindHook, NamespaceToken, RuntimeError};
380/// use serde_json::Value;
381///
382/// #[derive(Debug)]
383/// struct Sequencing;
384///
385/// #[async_trait]
386/// impl KindHook for Sequencing {
387/// async fn prepare_create(
388/// &self,
389/// _runtime: &KhiveRuntime,
390/// _args: &mut Value,
391/// ) -> Result<(), RuntimeError> {
392/// Ok(())
393/// }
394///
395/// async fn after_create(
396/// &self,
397/// _runtime: &KhiveRuntime,
398/// _id: uuid::Uuid,
399/// _args: &Value,
400/// ) -> Result<(), RuntimeError> {
401/// Ok(())
402/// }
403///
404/// async fn prepare_note_update(
405/// &self,
406/// _runtime: &KhiveRuntime,
407/// _token: &NamespaceToken,
408/// _note: &khive_storage::Note,
409/// _args: &mut Value,
410/// ) -> Result<(), RuntimeError> {
411/// Ok(())
412/// }
413/// }
414/// ```
415///
416/// The companion below is the control, and it is what makes the arm above mean anything: a
417/// `compile_fail` doctest passes when the code fails to compile for ANY reason, including a stale
418/// import or a renamed type. This one is structurally identical and overrides the two halves a pack
419/// is meant to implement, so it must compile — if it stops compiling, the arm above has stopped
420/// testing the method and is passing on the scaffolding instead.
421///
422/// ```
423/// use async_trait::async_trait;
424/// use khive_runtime::{KhiveRuntime, KindHook, NamespaceToken, RuntimeError};
425/// use serde_json::Value;
426///
427/// #[derive(Debug)]
428/// struct Halves;
429///
430/// #[async_trait]
431/// impl KindHook for Halves {
432/// async fn prepare_create(
433/// &self,
434/// _runtime: &KhiveRuntime,
435/// _args: &mut Value,
436/// ) -> Result<(), RuntimeError> {
437/// Ok(())
438/// }
439///
440/// async fn after_create(
441/// &self,
442/// _runtime: &KhiveRuntime,
443/// _id: uuid::Uuid,
444/// _args: &Value,
445/// ) -> Result<(), RuntimeError> {
446/// Ok(())
447/// }
448///
449/// async fn normalize_note_update(
450/// &self,
451/// _runtime: &KhiveRuntime,
452/// _token: &NamespaceToken,
453/// _note: &khive_storage::Note,
454/// _args: &mut Value,
455/// ) -> Result<(), RuntimeError> {
456/// Ok(())
457/// }
458///
459/// async fn validate_note_update(
460/// &self,
461/// _runtime: &KhiveRuntime,
462/// _token: &NamespaceToken,
463/// _note: &khive_storage::Note,
464/// _properties: Option<&Value>,
465/// ) -> Result<(), RuntimeError> {
466/// Ok(())
467/// }
468/// }
469/// ```
470#[async_trait]
471pub trait KindHook: Send + Sync + std::fmt::Debug {
472 /// Mutate args before the storage write. Fill defaults, normalize values,
473 /// rearrange user-facing fields into the storage shape expected by the
474 /// shared CRUD handler.
475 ///
476 /// Returning an error aborts the create call (no storage write happens).
477 async fn prepare_create(
478 &self,
479 runtime: &KhiveRuntime,
480 args: &mut Value,
481 ) -> Result<(), RuntimeError>;
482
483 /// Fire side effects after a successful storage write — graph edges,
484 /// derived observations, etc. The newly created record's UUID is passed
485 /// so the hook can attach metadata referencing it.
486 ///
487 /// Errors here are **logged but not propagated** — the storage write has
488 /// already succeeded; failing the call would mislead the caller.
489 /// Implementations should `tracing::warn!` and return `Ok(())` for
490 /// best-effort side effects. The default does nothing.
491 async fn after_create(
492 &self,
493 _runtime: &KhiveRuntime,
494 _id: uuid::Uuid,
495 _args: &Value,
496 ) -> Result<(), RuntimeError> {
497 Ok(())
498 }
499
500 /// Validate an approved AddEntity draft before preparing domain writes.
501 /// The draft kind is canonical.
502 /// This must not mutate storage or normalize the approved draft. The default
503 /// accepts it. This separate seam never invokes shared-create lifecycle
504 /// hooks and does not apply to AddNote; see `validate_proposal_note` below
505 /// for that route.
506 fn validate_proposal_entity(
507 &self,
508 _entity: &khive_types::EntityDraft,
509 ) -> Result<(), RuntimeError> {
510 Ok(())
511 }
512
513 /// Validate an `AddNote` draft on the proposal-note route, analogous to
514 /// [`Self::validate_proposal_entity`] but for notes. The kg pack's
515 /// proposal route calls this against the same immutable changeset at two
516 /// points: once when a new `propose` call is accepted, and again when an
517 /// approved proposal is applied, so a kind that refuses shared creation
518 /// is not bypassed by proposing the same creation instead. The draft's
519 /// kind is the owning pack's canonical spelling. This must not mutate
520 /// storage or normalize the draft; it only accepts or refuses. The
521 /// default accepts it.
522 fn validate_proposal_note(&self, _note: &khive_types::NoteDraft) -> Result<(), RuntimeError> {
523 Ok(())
524 }
525
526 /// Normalize caller-facing note-update fields before validation runs.
527 ///
528 /// Override this when a kind-owning pack's caller-facing note fields
529 /// mirror owned properties and must be changed together (for example, a
530 /// task's searchable `content` and `properties.description`). Validation
531 /// is not this method's job: the registry runs
532 /// [`Self::validate_note_update`] after this method returns, regardless of
533 /// what this method did. The default does nothing.
534 async fn normalize_note_update(
535 &self,
536 _runtime: &KhiveRuntime,
537 _token: &NamespaceToken,
538 _note: &khive_storage::Note,
539 _args: &mut Value,
540 ) -> Result<(), RuntimeError> {
541 Ok(())
542 }
543
544 /// Validate a shared note-property update before storage is mutated.
545 ///
546 /// The default accepts the update. Kind-owning packs override this when a
547 /// property has invariants that generic CRUD cannot know about (for
548 /// example, GTD task dependency acyclicity). This always runs after
549 /// [`Self::normalize_note_update`], because
550 /// [`VerbRegistry::prepare_note_update_policy`] calls them in that order.
551 async fn validate_note_update(
552 &self,
553 _runtime: &KhiveRuntime,
554 _token: &NamespaceToken,
555 _note: &khive_storage::Note,
556 _properties: Option<&Value>,
557 ) -> Result<(), RuntimeError> {
558 Ok(())
559 }
560
561 /// Describe graph changes coupled to a validated note patch, without writing.
562 ///
563 /// The dispatcher calls this only after normalization, kind validation, and
564 /// preparation of the note's guarded write. The runtime prepares these typed
565 /// effects and commits them with that write in one atomic unit. Implementors
566 /// must derive effects from this exact snapshot and patch; omitted or unchanged
567 /// owned fields should return no effects. This is not an after-update hook.
568 async fn note_update_effects(
569 &self,
570 _runtime: &KhiveRuntime,
571 _token: &NamespaceToken,
572 _note: &khive_storage::Note,
573 _patch: &crate::curation::NotePatch,
574 ) -> Result<Vec<NoteUpdateEffect>, RuntimeError> {
575 Ok(Vec::new())
576 }
577
578 /// Optional top-level properties whose explicit null update deletes the
579 /// stored key after the shared merge. Omission still preserves the key.
580 /// The default changes no property semantics. This policy is returned only
581 /// after normalization and validation have accepted the update.
582 fn note_update_null_clearing_properties(&self) -> &'static [&'static str] {
583 &[]
584 }
585
586 /// Validate a shared entity-property update before storage is mutated.
587 ///
588 /// Runs after the caller's patch has been merged into the entity's
589 /// stored properties, so `properties` reflects the resulting record
590 /// rather than the raw patch — the invariant this validates (e.g. "a
591 /// required key must be present and typed") is a claim about the
592 /// record, not about what one caller happened to send. This is
593 /// deliberately NOT a re-run of `prepare_create`: a `prepare_create`
594 /// body may also enforce create-shape requirements (an argument the
595 /// caller must supply at create time) that a partial update
596 /// legitimately omits, and re-running it would reject valid updates
597 /// with an error message written for create.
598 ///
599 /// The default accepts the update. Kind-owning packs override this when
600 /// a `prepare_create` invariant must also hold after a generic
601 /// `update`, sharing one predicate between both methods the way
602 /// [`validate_note_update`](Self::validate_note_update)'s implementors
603 /// already do for notes.
604 async fn validate_entity_update(
605 &self,
606 _runtime: &KhiveRuntime,
607 _token: &NamespaceToken,
608 _entity: &khive_storage::Entity,
609 _properties: Option<&Value>,
610 ) -> Result<(), RuntimeError> {
611 Ok(())
612 }
613
614 /// Validate one or more shared graph links before any edge is written.
615 ///
616 /// A batch is supplied as a unit so a hook can reject a cycle formed only
617 /// by the proposed edges. The default accepts every link.
618 async fn validate_links(
619 &self,
620 _runtime: &KhiveRuntime,
621 _token: &NamespaceToken,
622 _links: &[crate::LinkSpec],
623 ) -> Result<(), RuntimeError> {
624 Ok(())
625 }
626}
627
628/// A kind-owned graph mutation committed with its note update. SQL and deferred
629/// callbacks are deliberately not part of this interface.
630#[derive(Clone, Debug)]
631pub enum NoteUpdateEffect {
632 /// Create or explicitly resurrect an outgoing edge using normal link guards.
633 Link(LinkSpec),
634 /// Soft-delete one existing outgoing edge by ID, guarded by this snapshot.
635 DeleteEdge(khive_storage::types::Edge),
636 /// Preserve a live outgoing edge and assert its identity and endpoints at
637 /// commit time without changing its ID, timestamps, weight, or metadata.
638 AssertLink(khive_storage::types::Edge),
639}
640
641/// Optional sub-trait for packs that own private SQL tables and issue UUIDs
642/// that must be reachable through the generic `get(id)` and `delete(id)` verbs.
643///
644/// Implementing both methods is required — the sub-trait bundles them atomically
645/// so partial implementation is a compile-time error, not a runtime surprise.
646/// Packs whose records live in the shared entity/note substrate (gtd, memory)
647/// do not implement this sub-trait.
648#[async_trait]
649pub trait PackByIdResolver: Send + Sync {
650 /// Attempt to resolve a live (non-deleted) UUID owned by this pack's private tables.
651 ///
652 /// Returns `Some(Resolved::PackRecord { ... })` if this pack owns the UUID,
653 /// `None` if it does not (the caller continues to the next resolver),
654 /// or `Err(...)` on a storage error.
655 ///
656 /// Must query domain-authoritative tables before mirror tables.
657 /// Must NOT filter by namespace. UUID v4 is globally unique; by-ID
658 /// resolution is namespace-blind per ADR-007.
659 async fn resolve_by_id(
660 &self,
661 id: uuid::Uuid,
662 ) -> Result<Option<crate::Resolved>, crate::RuntimeError>;
663
664 /// Attempt to resolve a UUID including already-soft-deleted records.
665 ///
666 /// Used by the hard-delete path. Default delegates to `resolve_by_id`;
667 /// packs with `deleted_at` columns override this to query without the filter.
668 async fn resolve_by_id_including_deleted(
669 &self,
670 id: uuid::Uuid,
671 ) -> Result<Option<crate::Resolved>, crate::RuntimeError> {
672 self.resolve_by_id(id).await
673 }
674
675 /// Delete a record owned by this pack's private tables.
676 ///
677 /// `hard` mirrors the `delete` verb's `hard?` argument.
678 /// Default behavior for packs with a `deleted_at` column MUST be soft-delete;
679 /// `hard=true` performs permanent removal.
680 ///
681 /// Returns `Ok(Value)` with a `{ deleted: true, id, kind, hard }` body on success.
682 /// Returns `Err(RuntimeError::NotFound(...))` if the record does not exist.
683 async fn delete_by_id(
684 &self,
685 id: uuid::Uuid,
686 hard: bool,
687 ) -> Result<serde_json::Value, crate::RuntimeError>;
688
689 /// Verbs that change this pack's private records, named as examples when
690 /// a generic verb refuses one of them (ADR-061 Amendment 1).
691 ///
692 /// The refusal names the owning pack either way. A pack that returns
693 /// nothing is named without examples rather than borrowing another pack's.
694 fn private_record_verbs(&self) -> &'static [&'static str] {
695 &[]
696 }
697}