Skip to main content

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}