pub trait KindHook:
Send
+ Sync
+ Debug {
// Required methods
fn prepare_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
args: &'life2 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait;
fn after_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
id: Uuid,
args: &'life2 Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait;
// Provided methods
fn validate_proposal_entity(
&self,
_entity: &EntityDraft,
) -> Result<(), RuntimeError> { ... }
fn validate_proposal_note(
&self,
_note: &NoteDraft,
) -> Result<(), RuntimeError> { ... }
fn normalize_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_args: &'life4 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait { ... }
fn validate_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait { ... }
fn note_update_effects<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_patch: &'life4 NotePatch,
) -> Pin<Box<dyn Future<Output = Result<Vec<NoteUpdateEffect>, RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait { ... }
fn note_update_null_clearing_properties(&self) -> &'static [&'static str] { ... }
fn validate_entity_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_entity: &'life3 Entity,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait { ... }
fn validate_links<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_links: &'life3 [LinkSpec],
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait { ... }
}Expand description
Per-kind specialization for shared CRUD.
Packs implement KindHook for kinds they own that need:
- Defaults filled into create args (e.g.
status="inbox"for tasks) - Derived properties computed from args (e.g. salience from priority)
- Side-effect writes after the storage commit (e.g.
depends_onedges) - Cross-pack validation before shared CRUD mutates an owned kind
Hooks are stateless from the framework’s perspective — they receive the
runtime and the current mutation inputs as method parameters. The pack
registers them via PackRuntime::kind_hook.
Lifecycle verbs (e.g. gtd’s complete, transition) remain pack-owned
verbs. Shared create, note update, entity update, and link calls
flow through this trait when an endpoint kind has an owning pack hook.
A hook that still overrides the removed sequencing method does not compile, which is the point of the move: an implementor cannot replace the validator by replacing the sequence, because there is no sequence on this trait to replace.
use async_trait::async_trait;
use khive_runtime::{KhiveRuntime, KindHook, NamespaceToken, RuntimeError};
use serde_json::Value;
#[derive(Debug)]
struct Sequencing;
#[async_trait]
impl KindHook for Sequencing {
async fn prepare_create(
&self,
_runtime: &KhiveRuntime,
_args: &mut Value,
) -> Result<(), RuntimeError> {
Ok(())
}
async fn after_create(
&self,
_runtime: &KhiveRuntime,
_id: uuid::Uuid,
_args: &Value,
) -> Result<(), RuntimeError> {
Ok(())
}
async fn prepare_note_update(
&self,
_runtime: &KhiveRuntime,
_token: &NamespaceToken,
_note: &khive_storage::Note,
_args: &mut Value,
) -> Result<(), RuntimeError> {
Ok(())
}
}The companion below is the control, and it is what makes the arm above mean anything: a
compile_fail doctest passes when the code fails to compile for ANY reason, including a stale
import or a renamed type. This one is structurally identical and overrides the two halves a pack
is meant to implement, so it must compile — if it stops compiling, the arm above has stopped
testing the method and is passing on the scaffolding instead.
use async_trait::async_trait;
use khive_runtime::{KhiveRuntime, KindHook, NamespaceToken, RuntimeError};
use serde_json::Value;
#[derive(Debug)]
struct Halves;
#[async_trait]
impl KindHook for Halves {
async fn prepare_create(
&self,
_runtime: &KhiveRuntime,
_args: &mut Value,
) -> Result<(), RuntimeError> {
Ok(())
}
async fn after_create(
&self,
_runtime: &KhiveRuntime,
_id: uuid::Uuid,
_args: &Value,
) -> Result<(), RuntimeError> {
Ok(())
}
async fn normalize_note_update(
&self,
_runtime: &KhiveRuntime,
_token: &NamespaceToken,
_note: &khive_storage::Note,
_args: &mut Value,
) -> Result<(), RuntimeError> {
Ok(())
}
async fn validate_note_update(
&self,
_runtime: &KhiveRuntime,
_token: &NamespaceToken,
_note: &khive_storage::Note,
_properties: Option<&Value>,
) -> Result<(), RuntimeError> {
Ok(())
}
}Required Methods§
Sourcefn prepare_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
args: &'life2 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn prepare_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
args: &'life2 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Mutate args before the storage write. Fill defaults, normalize values, rearrange user-facing fields into the storage shape expected by the shared CRUD handler.
Returning an error aborts the create call (no storage write happens).
Sourcefn after_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
id: Uuid,
args: &'life2 Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn after_create<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
runtime: &'life1 KhiveRuntime,
id: Uuid,
args: &'life2 Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Fire side effects after a successful storage write — graph edges, derived observations, etc. The newly created record’s UUID is passed so the hook can attach metadata referencing it.
Errors here are logged but not propagated — the storage write has
already succeeded; failing the call would mislead the caller.
Implementations should tracing::warn! and return Ok(()) for
best-effort side effects.
Provided Methods§
Sourcefn validate_proposal_entity(
&self,
_entity: &EntityDraft,
) -> Result<(), RuntimeError>
fn validate_proposal_entity( &self, _entity: &EntityDraft, ) -> Result<(), RuntimeError>
Validate an approved AddEntity draft before preparing domain writes.
The draft kind is canonical.
This must not mutate storage or normalize the approved draft. The default
accepts it. This separate seam never invokes shared-create lifecycle
hooks and does not apply to AddNote; see validate_proposal_note below
for that route.
Sourcefn validate_proposal_note(&self, _note: &NoteDraft) -> Result<(), RuntimeError>
fn validate_proposal_note(&self, _note: &NoteDraft) -> Result<(), RuntimeError>
Validate an AddNote draft on the proposal-note route, analogous to
Self::validate_proposal_entity but for notes. The kg pack’s
proposal route calls this against the same immutable changeset at two
points: once when a new propose call is accepted, and again when an
approved proposal is applied, so a kind that refuses shared creation
is not bypassed by proposing the same creation instead. The draft’s
kind is the owning pack’s canonical spelling. This must not mutate
storage or normalize the draft; it only accepts or refuses. The
default accepts it.
Sourcefn normalize_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_args: &'life4 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
fn normalize_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_args: &'life4 mut Value,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Normalize caller-facing note-update fields before validation runs.
Override this when a kind-owning pack’s caller-facing note fields
mirror owned properties and must be changed together (for example, a
task’s searchable content and properties.description). Validation
is not this method’s job: the registry runs
Self::validate_note_update after this method returns, regardless of
what this method did. The default does nothing.
Sourcefn validate_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
fn validate_note_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Validate a shared note-property update before storage is mutated.
The default accepts the update. Kind-owning packs override this when a
property has invariants that generic CRUD cannot know about (for
example, GTD task dependency acyclicity). This always runs after
Self::normalize_note_update, because
VerbRegistry::prepare_note_update_policy calls them in that order.
Sourcefn note_update_effects<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_patch: &'life4 NotePatch,
) -> Pin<Box<dyn Future<Output = Result<Vec<NoteUpdateEffect>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
fn note_update_effects<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_note: &'life3 Note,
_patch: &'life4 NotePatch,
) -> Pin<Box<dyn Future<Output = Result<Vec<NoteUpdateEffect>, RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Describe graph changes coupled to a validated note patch, without writing.
The dispatcher calls this only after normalization, kind validation, and preparation of the note’s guarded write. The runtime prepares these typed effects and commits them with that write in one atomic unit. Implementors must derive effects from this exact snapshot and patch; omitted or unchanged owned fields should return no effects. This is not an after-update hook.
Sourcefn note_update_null_clearing_properties(&self) -> &'static [&'static str]
fn note_update_null_clearing_properties(&self) -> &'static [&'static str]
Optional top-level properties whose explicit null update deletes the stored key after the shared merge. Omission still preserves the key. The default changes no property semantics. This policy is returned only after normalization and validation have accepted the update.
Sourcefn validate_entity_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_entity: &'life3 Entity,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
fn validate_entity_update<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_entity: &'life3 Entity,
_properties: Option<&'life4 Value>,
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Validate a shared entity-property update before storage is mutated.
Runs after the caller’s patch has been merged into the entity’s
stored properties, so properties reflects the resulting record
rather than the raw patch — the invariant this validates (e.g. “a
required key must be present and typed”) is a claim about the
record, not about what one caller happened to send. This is
deliberately NOT a re-run of prepare_create: a prepare_create
body may also enforce create-shape requirements (an argument the
caller must supply at create time) that a partial update
legitimately omits, and re-running it would reject valid updates
with an error message written for create.
The default accepts the update. Kind-owning packs override this when
a prepare_create invariant must also hold after a generic
update, sharing one predicate between both methods the way
validate_note_update’s implementors
already do for notes.
Sourcefn validate_links<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_links: &'life3 [LinkSpec],
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn validate_links<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_runtime: &'life1 KhiveRuntime,
_token: &'life2 NamespaceToken,
_links: &'life3 [LinkSpec],
) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Validate one or more shared graph links before any edge is written.
A batch is supplied as a unit so a hook can reject a cycle formed only by the proposed edges. The default accepts every link.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".