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