Skip to main content

KindHook

Trait KindHook 

Source
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_on edges)
  • 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§

Source

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).

Source

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§

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

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".

Implementors§