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 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}