use crate::edge::EdgeRelation;
use crate::entity_type::EntityTypeDef;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Visibility {
Verb,
Subhandler,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VerbCategory {
Assertive,
Directive,
Commissive,
Declaration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ParamDef {
pub name: &'static str,
pub param_type: &'static str,
pub required: bool,
pub description: &'static str,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HandlerDef {
pub name: &'static str,
pub description: &'static str,
pub visibility: Visibility,
pub category: VerbCategory,
pub params: &'static [ParamDef],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum VerbPresentationPolicy {
#[default]
Standard,
AlwaysVerbose,
}
impl HandlerDef {
pub fn presentation_policy(&self) -> VerbPresentationPolicy {
match self.name {
"get" | "link" | "query" | "traverse" | "neighbors" | "brain.feedback" => {
VerbPresentationPolicy::AlwaysVerbose
}
_ => VerbPresentationPolicy::Standard,
}
}
}
#[deprecated(since = "0.2.0", note = "Use HandlerDef instead")]
pub type VerbDef = HandlerDef;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EndpointKind {
NoteOfKind(&'static str),
EntityOfKind(&'static str),
EntityOfType {
kind: &'static str,
entity_type: &'static str,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EdgeEndpointRule {
pub relation: EdgeRelation,
pub source: EndpointKind,
pub target: EndpointKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoteLifecycleSpec {
pub field: &'static str,
pub initial: &'static str,
pub terminal: &'static [&'static str],
pub transitions: &'static [(&'static str, &'static str)],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoteKindSpec {
pub kind: &'static str,
pub aliases: &'static [&'static str],
pub lifecycle: NoteLifecycleSpec,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackSchemaPlan {
pub pack: &'static str,
pub statements: &'static [&'static str],
}
pub trait Pack {
const NAME: &'static str;
const NOTE_KINDS: &'static [&'static str];
const ENTITY_KINDS: &'static [&'static str];
const HANDLERS: &'static [HandlerDef];
const EDGE_RULES: &'static [EdgeEndpointRule] = &[];
const ENTITY_TYPES: &'static [EntityTypeDef] = &[];
const REQUIRES: &'static [&'static str] = &[];
const NOTE_KIND_SPECS: &'static [NoteKindSpec] = &[];
const SCHEMA_PLAN: Option<PackSchemaPlan> = None;
const VALIDATION_RULES: &'static [&'static str] = &[];
}
pub const ATOMIC_ADMISSIBLE_VERBS: &[&str] = &[
"update",
"delete",
"link",
"merge",
"gtd.transition",
"gtd.complete",
"propose",
"review",
"withdraw",
];
const ATOMIC_EMBEDDING_BEARING_VERBS: &[&str] = &[
"create",
"memory.remember",
"gtd.assign",
"comm.send",
"comm.reply",
"comm.ingest",
];
pub const ATOMIC_KNOWN_UNIMPLEMENTED_VERBS: &[&str] = &["propose", "review", "withdraw", "merge"];
const ATOMIC_READ_VERBS: &[&str] = &[
"search",
"recall",
"query",
"traverse",
"list",
"get",
"neighbors",
"context",
"stats",
"verbs",
];
pub const ATOMIC_MAX_OPS_DEFAULT: usize = 2000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AtomicRejectionReason {
EmbeddingBearing,
Read,
Unlisted,
KnownUnimplemented,
}
pub fn atomic_admissibility(verb_name: &str) -> Option<AtomicRejectionReason> {
if ATOMIC_KNOWN_UNIMPLEMENTED_VERBS.contains(&verb_name) {
return Some(AtomicRejectionReason::KnownUnimplemented);
}
if ATOMIC_ADMISSIBLE_VERBS.contains(&verb_name) {
return None;
}
if ATOMIC_EMBEDDING_BEARING_VERBS.contains(&verb_name) {
return Some(AtomicRejectionReason::EmbeddingBearing);
}
if ATOMIC_READ_VERBS.contains(&verb_name) {
return Some(AtomicRejectionReason::Read);
}
Some(AtomicRejectionReason::Unlisted)
}
#[cfg(test)]
mod tests {
use super::*;
struct TestPack;
impl Pack for TestPack {
const NAME: &'static str = "test";
const NOTE_KINDS: &'static [&'static str] = &["memo"];
const ENTITY_KINDS: &'static [&'static str] = &["widget"];
const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
name: "do_thing",
description: "does a thing",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[],
}];
}
#[test]
fn pack_trait_compiles() {
assert_eq!(TestPack::NAME, "test");
assert_eq!(TestPack::NOTE_KINDS, &["memo"]);
assert_eq!(TestPack::ENTITY_KINDS, &["widget"]);
assert_eq!(TestPack::HANDLERS.len(), 1);
assert_eq!(TestPack::HANDLERS[0].name, "do_thing");
assert_eq!(TestPack::HANDLERS[0].visibility, Visibility::Verb);
assert_eq!(TestPack::HANDLERS[0].category, VerbCategory::Commissive);
}
#[test]
fn verb_category_variants_exist() {
let _ = VerbCategory::Assertive;
let _ = VerbCategory::Directive;
let _ = VerbCategory::Commissive;
let _ = VerbCategory::Declaration;
}
#[test]
fn pack_validation_rules_default_empty() {
assert!(TestPack::VALIDATION_RULES.is_empty());
}
#[test]
fn pack_entity_types_default_empty() {
assert!(TestPack::ENTITY_TYPES.is_empty());
}
#[test]
fn link_handler_is_always_verbose() {
let link_def = HandlerDef {
name: "link",
description: "Create a typed directed edge",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[],
};
assert_eq!(
link_def.presentation_policy(),
VerbPresentationPolicy::AlwaysVerbose,
"link must be AlwaysVerbose"
);
}
#[test]
fn always_verbose_set_contains_expected_verbs() {
let always_verbose = [
"get",
"link",
"query",
"traverse",
"neighbors",
"brain.feedback",
];
for name in always_verbose {
let h = HandlerDef {
name,
description: "",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[],
};
assert_eq!(
h.presentation_policy(),
VerbPresentationPolicy::AlwaysVerbose,
"{name:?} must be AlwaysVerbose"
);
}
}
#[test]
fn non_verbose_verbs_are_standard_policy() {
let standard = [
"create", "list", "update", "delete", "search", "recall", "remember",
];
for name in standard {
let h = HandlerDef {
name,
description: "",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[],
};
assert_eq!(
h.presentation_policy(),
VerbPresentationPolicy::Standard,
"{name:?} must be Standard (not AlwaysVerbose)"
);
}
}
#[test]
fn atomic_admissible_list_matches_adr099_d3() {
let adr_099_d3_v1_admissible_set: &[&str] = &[
"update",
"delete",
"link",
"merge",
"gtd.transition",
"gtd.complete",
"propose",
"review",
"withdraw",
];
assert_eq!(
ATOMIC_ADMISSIBLE_VERBS, adr_099_d3_v1_admissible_set,
"ATOMIC_ADMISSIBLE_VERBS drifted from ADR-099 D3's explicit v1 list"
);
}
#[test]
fn atomic_admissible_verbs_are_admitted() {
for verb in ATOMIC_ADMISSIBLE_VERBS {
if ATOMIC_KNOWN_UNIMPLEMENTED_VERBS.contains(verb) {
continue;
}
assert_eq!(
atomic_admissibility(verb),
None,
"{verb:?} is on the v1 admissible list and must be admitted"
);
}
}
#[test]
fn atomic_known_unimplemented_verbs_rejected_before_runtime() {
for verb in ATOMIC_KNOWN_UNIMPLEMENTED_VERBS {
assert!(
ATOMIC_ADMISSIBLE_VERBS.contains(verb),
"{verb:?} must remain on ATOMIC_ADMISSIBLE_VERBS per ADR-099 D3"
);
assert_eq!(
atomic_admissibility(verb),
Some(AtomicRejectionReason::KnownUnimplemented),
"{verb:?} must be rejected as known-unimplemented, not admitted"
);
}
}
#[test]
fn atomic_embedding_bearing_verbs_rejected_named() {
for verb in [
"create",
"memory.remember",
"gtd.assign",
"comm.send",
"comm.reply",
] {
assert_eq!(
atomic_admissibility(verb),
Some(AtomicRejectionReason::EmbeddingBearing),
"{verb:?} must be rejected as embedding-bearing (ADR-099 acceptance criteria)"
);
}
}
#[test]
fn atomic_read_verbs_rejected() {
for verb in [
"search",
"recall",
"query",
"traverse",
"list",
"get",
"neighbors",
"context",
] {
assert_eq!(
atomic_admissibility(verb),
Some(AtomicRejectionReason::Read),
"{verb:?} must be rejected as a read verb"
);
}
}
#[test]
fn atomic_unknown_verb_defaults_to_unlisted_rejection() {
assert_eq!(
atomic_admissibility("some_future_verb_nobody_classified_yet"),
Some(AtomicRejectionReason::Unlisted),
"an unrecognized verb must default-deny, never silently admit"
);
}
}