use crate::audit_bridge::{AUDIT_DB_FILE, StoreAuditSink};
use crate::hostload;
use crate::usage;
use crate::writers::{
FsNoteWriter, IndexWriter, NoopIndexWriter, NoopNoteWriter, NoteWriter, SqliteIndexWriter,
StoreEraser, lock_index,
};
use cyberbrain_core::Slash;
use cyberbrain_core::blocks::{MAX_BLOCK_TOKENS, OversizedReason, blocks_of};
use cyberbrain_core::config::DEFAULT_STORE_DIR;
use cyberbrain_core::frontmatter;
use cyberbrain_core::links::link_targets;
use cyberbrain_core::store::{DB_FILE, NOTES_DIR, write_atomic};
use cyberbrain_core::{
Citation, Config, EgressGate, Embedder, Error, Frontmatter, Note, NoteId, NoteKind, PiiState,
RecallResult, Result, Ring, Store,
};
use cyberbrain_embed::{ArtefactManifest, ModelPaths, StaticEmbedder};
use cyberbrain_index::{
AuditStore, EmbeddingProfile, Erased, Index, IndexStats, NoteStamp, ProfileChange,
RecallOptions, content_hash,
};
use cyberbrain_llm::{LlmClient, LlmConfig, Probe};
use cyberbrain_policy::profile::ProfileExt;
use cyberbrain_policy::{
Actor, AuditAction, AuditFilter, EgressEntry, EraseReason, EraseRequest, ErasureReport,
ExportFormat, Finding, Identifier, MemoryAuditSink, ModelCard, ModelInventory, ModelRole,
OperatorChoice, Policy, PolicyConfig, PolicyStatus, RetentionItem, RetentionQueue,
SubjectAccessReport, SubjectBlock, SubjectSource, WriteVerdict,
};
use serde::Serialize;
use serde_json::json;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
pub const MANIFEST_FILE: &str = "manifest.json";
fn is_store(p: &Path) -> bool {
p.join(NOTES_DIR).is_dir()
}
pub fn discover_store(explicit: Option<&Path>) -> Result<PathBuf> {
if let Some(p) = explicit {
if is_store(p) {
return Ok(p.to_path_buf());
}
return Err(Error::Config(format!(
"{} is not a cyberbrain store (no notes/ directory inside it); create one with \
`cyberbrain init --path {}`",
Slash(p),
Slash(p)
)));
}
discover_store_from(None)
}
pub fn discover_store_from(start: Option<&Path>) -> Result<PathBuf> {
let cwd = match start {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().map_err(|e| Error::Io {
path: PathBuf::from("."),
source: e,
})?,
};
for dir in cwd.ancestors() {
let candidate = dir.join(DEFAULT_STORE_DIR);
if is_store(&candidate) {
return Ok(candidate);
}
}
Err(Error::Config(format!(
"no {DEFAULT_STORE_DIR} store found in {} or any directory above it; run \
`cyberbrain init` in the project root, or point at one with --store or \
CYBERBRAIN_STORE",
Slash(&cwd)
)))
}
#[derive(Debug, Clone, Serialize)]
pub struct InitReport {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub store: PathBuf,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub config: PathBuf,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub audit_db: PathBuf,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub index_db: PathBuf,
pub next_steps: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SkippedFile {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub reason: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct OversizedBlock {
pub note: String,
pub block_idx: u32,
pub approx_tokens: u32,
pub reason: &'static str,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct EmbedderSummary {
pub loaded: bool,
pub profile_id: Option<String>,
pub dim: Option<usize>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ScanOptions {
pub full: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ScanReport {
pub dry_run: bool,
pub full: bool,
pub files_listed: usize,
pub indexed_new: usize,
pub reindexed_changed: usize,
pub revectorised: usize,
pub unchanged: usize,
pub touched_only: usize,
pub dropped_missing_file: Vec<String>,
pub skipped: Vec<SkippedFile>,
pub links_written_back: usize,
pub link_writeback_failed: Vec<String>,
pub oversized_blocks: Vec<OversizedBlock>,
pub embedder: EmbedderSummary,
pub profile_change: Option<ProfileChange>,
pub cleared: Option<Erased>,
pub index: IndexStats,
pub audit_preview: Vec<String>,
pub elapsed_ms: u128,
}
#[derive(Debug, Clone, Serialize)]
pub struct NoteView {
pub front: Frontmatter,
pub kind: String,
pub body: String,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub blocks: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct BlockView {
pub citation: String,
pub idx: u32,
pub text: String,
pub token_count: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct Expanded {
pub citation: String,
pub block: BlockView,
pub note: NoteView,
}
#[derive(Debug, Clone, Default)]
pub struct RecallRequest {
pub n: Option<usize>,
pub ring: Option<Ring>,
}
#[derive(Debug, Clone)]
pub struct WriteRequest {
pub ring: Ring,
pub kind: NoteKind,
pub name: String,
pub body: String,
pub tags: Vec<String>,
pub retention: Option<String>,
pub force: bool,
pub choice: Option<OperatorChoice>,
pub expected_updated: Option<jiff::Timestamp>,
pub dry_run: bool,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum Proposed {
Written(ProposeReport),
Held {
rendered: String,
name: String,
#[serde(skip)]
findings: Vec<cyberbrain_policy::Finding>,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct ManifestReport {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub weights_blake3: String,
pub tokenizer_blake3: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProposeReport {
pub name: String,
pub ring: Ring,
pub kind: String,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub proposed_by: String,
pub bytes: usize,
pub pii: PiiState,
pub redacted: usize,
pub changes_existing: bool,
pub dry_run: bool,
pub audit_preview: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProposalSummary {
pub name: String,
pub ring: Ring,
pub kind: String,
pub proposed_by: Option<String>,
pub created: jiff::Timestamp,
pub changes_existing: bool,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct ReviewRequest {
pub name: String,
pub accept: bool,
pub reason: String,
pub by: String,
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ReviewReport {
pub name: String,
pub accepted: bool,
pub by: String,
pub proposed_by: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "cyberbrain_core::path_serde::slash_opt"
)]
pub path: Option<PathBuf>,
pub blocks: usize,
pub vectors: usize,
pub dry_run: bool,
pub audit_preview: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct WrittenNote {
pub id: NoteId,
pub name: String,
pub ring: Ring,
pub kind: String,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub bytes: usize,
pub created: bool,
pub updated: jiff::Timestamp,
pub pii: PiiState,
pub redacted: usize,
pub blocks: usize,
pub vectors: usize,
pub links: usize,
pub embedder_reason: Option<String>,
pub dry_run: bool,
pub audit_preview: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "outcome", rename_all = "kebab-case")]
pub enum WriteOutcome {
Written(WrittenNote),
Held {
name: String,
findings: Vec<Finding>,
rendered: String,
},
Conflict {
name: String,
current_updated: jiff::Timestamp,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct DoctorFinding {
pub severity: &'static str,
pub check: &'static str,
pub detail: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DoctorReport {
pub clean: bool,
pub checks_run: Vec<&'static str>,
pub findings: Vec<DoctorFinding>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AuditSummary {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub rows: usize,
pub schema_version: u32,
pub chain: std::result::Result<usize, String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct EmbeddingStatus {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub model_dir: PathBuf,
pub manifest_present: bool,
pub embedder: EmbedderSummary,
pub index_profile: Option<EmbeddingProfile>,
pub matches_index: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct InferenceStatus {
pub endpoint: String,
pub model: Option<String>,
pub state: String,
pub probe: Option<Probe>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StatusReport {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub store: PathBuf,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub config: PathBuf,
pub notes_on_disk: usize,
pub notes_per_ring: [usize; 5],
pub files_skipped: usize,
pub resident_tokens: usize,
pub resident_cap: usize,
pub index: IndexStats,
pub index_stale: bool,
pub audit: AuditSummary,
pub embedding: EmbeddingStatus,
pub inference: InferenceStatus,
pub policy: PolicyStatus,
}
#[derive(Debug, Clone, Serialize)]
pub struct AuditView {
pub rows: usize,
pub verified: Option<std::result::Result<usize, String>>,
pub rendered: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ObligationsView {
pub profile: cyberbrain_core::PolicyProfile,
pub law: String,
pub obligations: Vec<cyberbrain_policy::profile::Obligation>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RetentionOutcome {
pub name: String,
pub item: RetentionItem,
pub result: std::result::Result<ErasureReport, String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RetentionReport {
pub queue: RetentionQueue,
pub unreadable: Vec<String>,
pub applied_run: bool,
pub dry_run: bool,
pub applied: Vec<RetentionOutcome>,
pub audit_preview: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelCardReport {
pub cards: Vec<ModelCard>,
pub absent: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConsentReport {
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub path: PathBuf,
pub consent: bool,
pub model_source: Option<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FindHit {
pub path: String,
pub start_line: u32,
pub end_line: u32,
pub line: u32,
pub kind: &'static str,
pub language: &'static str,
pub name: String,
pub scope: Option<String>,
pub matched: &'static str,
pub snippet: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct FindSkipped {
pub ignored_entries: usize,
pub gitignored_entries: usize,
pub hidden_entries: usize,
pub store_entries: usize,
pub symlinks: usize,
pub lockfiles: usize,
pub too_large: usize,
pub binary: usize,
pub unsupported: usize,
pub unsupported_by_extension: std::collections::BTreeMap<String, usize>,
pub unreadable: Vec<SkippedFile>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FindReport {
pub symbol: String,
pub name: String,
pub scope: Option<String>,
#[serde(serialize_with = "cyberbrain_core::path_serde::slash")]
pub root: PathBuf,
pub hits: Vec<FindHit>,
pub matched_total: usize,
pub truncated: bool,
pub limit: usize,
pub files_scanned: usize,
pub bytes_scanned: u64,
pub definitions_indexed: usize,
pub skipped: FindSkipped,
pub ignore_files: Vec<String>,
pub caveats: Vec<String>,
pub elapsed_ms: u128,
}
impl From<cyberbrain_code::FindResult> for FindReport {
fn from(r: cyberbrain_code::FindResult) -> Self {
FindReport {
symbol: r.symbol,
name: r.name,
scope: r.scope,
root: r.root,
hits: r
.hits
.into_iter()
.map(|h| FindHit {
path: h.def.path,
start_line: h.def.start_line,
end_line: h.def.end_line,
line: h.def.line,
kind: h.def.kind.as_str(),
language: h.def.language.as_str(),
name: h.def.name,
scope: h.def.scope,
matched: h.matched.as_str(),
snippet: h.def.snippet,
})
.collect(),
matched_total: r.matched_total,
truncated: r.truncated,
limit: r.limit,
files_scanned: r.files_scanned,
bytes_scanned: r.bytes_scanned,
definitions_indexed: r.definitions_indexed,
skipped: FindSkipped {
ignored_entries: r.skipped.ignored_entries,
gitignored_entries: r.skipped.gitignored_entries,
hidden_entries: r.skipped.hidden_entries,
store_entries: r.skipped.excluded_entries,
symlinks: r.skipped.symlinks,
lockfiles: r.skipped.lockfiles,
too_large: r.skipped.too_large,
binary: r.skipped.binary,
unsupported: r.skipped.unsupported,
unsupported_by_extension: r.skipped.unsupported_by_extension,
unreadable: r
.skipped
.unreadable
.into_iter()
.map(|(path, reason)| SkippedFile {
path: PathBuf::from(path),
reason,
})
.collect(),
},
ignore_files: r.ignore_files,
caveats: r.caveats,
elapsed_ms: r.elapsed.as_millis(),
}
}
}
enum EmbedderState {
Loaded {
embedder: Arc<StaticEmbedder>,
manifest: ArtefactManifest,
paths: ModelPaths,
},
Absent {
reason: String,
},
}
#[derive(Clone)]
enum LlmState {
Ready(Box<LlmClient>),
Absent(String),
}
enum PolicyRef<'a> {
Real(&'a Policy),
Dry(Box<Policy>, Arc<MemoryAuditSink>),
}
impl PolicyRef<'_> {
fn get(&self) -> &Policy {
match self {
PolicyRef::Real(p) => p,
PolicyRef::Dry(p, _) => p,
}
}
fn preview(&self) -> Vec<String> {
match self {
PolicyRef::Real(_) => Vec::new(),
PolicyRef::Dry(_, sink) => sink.actions(),
}
}
}
struct Writers<'a> {
notes: Box<dyn NoteWriter>,
index: Box<dyn IndexWriter>,
policy: PolicyRef<'a>,
}
fn kind_name(k: NoteKind) -> String {
match serde_json::to_value(k) {
Ok(serde_json::Value::String(s)) => s,
_ => format!("{k:?}").to_lowercase(),
}
}
fn stamp_of(path: &Path) -> Option<NoteStamp> {
let md = std::fs::metadata(path).ok()?;
let ns = md
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
Some(NoteStamp {
mtime_ns: i64::try_from(ns).ok()?,
size: md.len(),
})
}
fn oversized_reason(r: OversizedReason) -> &'static str {
match r {
OversizedReason::CodeFence => "a fenced code block is never split",
OversizedReason::UnbreakableRun => "a single run of characters longer than the limit",
}
}
fn policy_config(cfg: &Config) -> PolicyConfig {
let mut pc = PolicyConfig::from_core(cfg)
.with_model_download_consent(cfg.embedding.model_download_consent);
if let Some(src) = &cfg.embedding.model_source {
pc = pc.with_model_source(src.clone());
}
pc
}
pub struct App {
root: PathBuf,
config: Config,
store: Store,
audit_sink: Arc<StoreAuditSink>,
policy: Policy,
gate: Arc<dyn EgressGate>,
index: Arc<Mutex<Index>>,
actor: Actor,
embedder: OnceLock<EmbedderState>,
llm: Mutex<Option<LlmState>>,
}
const TASK_CONTRADICTION: &str = "contradiction-check";
const TASK_CONTRADICTION_ABANDONED: &str = "contradiction-check-abandoned";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LastCheck {
Unknown,
Completed { ms: u64 },
Abandoned,
}
#[derive(Debug, PartialEq, Eq)]
enum CheckPlan {
Run(std::time::Duration),
SkipMeasuredSlow { last_ms: u64, budget_ms: u64 },
SkipAbandoned { budget_ms: u64 },
RunUnbounded,
}
fn plan_contradiction_check(budget_ms: u64, last: LastCheck) -> CheckPlan {
if budget_ms == 0 {
return CheckPlan::RunUnbounded;
}
match last {
LastCheck::Abandoned => CheckPlan::SkipAbandoned { budget_ms },
LastCheck::Completed { ms } if ms > budget_ms => CheckPlan::SkipMeasuredSlow {
last_ms: ms,
budget_ms,
},
_ => CheckPlan::Run(std::time::Duration::from_millis(budget_ms)),
}
}
const CHECK_MEASUREMENT_GOOD_FOR: std::time::Duration = std::time::Duration::from_secs(86_400);
fn stale(at: &str) -> bool {
let Ok(then) = at.parse::<jiff::Timestamp>() else {
return true;
};
jiff::Timestamp::now().duration_since(then).unsigned_abs() > CHECK_MEASUREMENT_GOOD_FOR
}
fn secs(ms: u64) -> String {
if ms < 1000 {
format!("{ms} ms")
} else if ms < 10_000 {
format!("{:.1} s", ms as f64 / 1000.0)
} else {
format!("{} s", ms / 1000)
}
}
impl App {
pub fn open(store: Option<&Path>, actor: Actor) -> Result<App> {
Self::open_from(store, None, actor)
}
pub fn open_from(store: Option<&Path>, start: Option<&Path>, actor: Actor) -> Result<App> {
let root = match store {
Some(_) => discover_store(store)?,
None => discover_store_from(start)?,
};
let config = Config::load(&root)?;
let store = Store::with_config(&root, &config)?;
let audit_sink = Arc::new(StoreAuditSink::new(AuditStore::open(
&root.join(AUDIT_DB_FILE),
)?));
let policy = Policy::new(policy_config(&config), audit_sink.clone(), actor.clone());
let gate = policy.gate();
let index = Index::open(&root.join(DB_FILE))?;
Ok(App {
root,
config,
store,
audit_sink,
policy,
gate,
index: Arc::new(Mutex::new(index)),
actor,
embedder: OnceLock::new(),
llm: Mutex::new(None),
})
}
pub fn init(path: &Path, actor: &Actor) -> Result<InitReport> {
if Config::path_in(path).exists() || is_store(path) {
return Err(Error::Config(format!(
"{} is already a cyberbrain store; nothing was changed",
Slash(path)
)));
}
let cap = Config::default().rings.resident_cap_tokens;
Store::create(path, cap)?;
let config = Config::write_default(path)?;
let audit_db = path.join(AUDIT_DB_FILE);
let sink = Arc::new(StoreAuditSink::new(AuditStore::open(&audit_db)?));
let index_db = path.join(DB_FILE);
let _ = Index::open(&index_db)?;
let cfg = Config::load(path)?;
let policy = Policy::new(policy_config(&cfg), sink, actor.clone());
policy.audit().record_raw(
&actor.to_string(),
"store.init",
format!("store:{}", Slash(path)),
json!({ "profile": cfg.policy.profile }),
)?;
Ok(InitReport {
store: path.to_path_buf(),
config,
audit_db,
index_db,
next_steps: vec![
"write a note: cyberbrain write --ring 2 --kind knowledge --name first-note --body 'what you learned'"
.into(),
"index the tree: cyberbrain scan".into(),
"search it: cyberbrain recall 'what you learned'".into(),
format!(
"for semantic search, place model.safetensors, tokenizer.json and {MANIFEST_FILE} under {}",
Slash(&cfg.model_dir())
),
"read the compliance profile: cyberbrain policy egress".into(),
],
})
}
#[allow(dead_code)]
pub fn root(&self) -> &Path {
&self.root
}
#[allow(dead_code)]
pub fn config(&self) -> &Config {
&self.config
}
#[allow(dead_code)]
pub fn store(&self) -> &Store {
&self.store
}
#[allow(dead_code)]
pub fn policy(&self) -> &Policy {
&self.policy
}
#[allow(dead_code)]
pub fn gate(&self) -> Arc<dyn EgressGate> {
self.gate.clone()
}
#[allow(dead_code)]
pub fn actor(&self) -> &Actor {
&self.actor
}
fn writers(&self, dry_run: bool) -> Writers<'_> {
if dry_run {
let sink = Arc::new(MemoryAuditSink::new());
let policy = Policy::new(
policy_config(&self.config),
sink.clone(),
self.actor.clone(),
);
Writers {
notes: Box::new(NoopNoteWriter {
store: self.store.clone(),
}),
index: Box::new(NoopIndexWriter {
index: self.index.clone(),
}),
policy: PolicyRef::Dry(Box::new(policy), sink),
}
} else {
Writers {
notes: Box::new(FsNoteWriter {
store: self.store.clone(),
}),
index: Box::new(SqliteIndexWriter {
index: self.index.clone(),
}),
policy: PolicyRef::Real(&self.policy),
}
}
}
fn embedder(&self) -> &EmbedderState {
self.embedder.get_or_init(|| self.load_embedder())
}
fn load_embedder(&self) -> EmbedderState {
let dir = self.config.model_dir();
let paths = ModelPaths::in_dir(&dir);
let manifest_path = dir.join(MANIFEST_FILE);
if !paths.weights.is_file() || !paths.tokenizer.is_file() {
return EmbedderState::Absent {
reason: format!(
"no model artefact at {} (expected model.safetensors and tokenizer.json); \
search is lexical only",
Slash(&dir)
),
};
}
let manifest: ArtefactManifest = match std::fs::read_to_string(&manifest_path) {
Ok(text) => match serde_json::from_str(&text) {
Ok(m) => m,
Err(e) => {
return EmbedderState::Absent {
reason: format!("{} is not a manifest: {e}", Slash(&manifest_path)),
};
}
},
Err(_) => {
return EmbedderState::Absent {
reason: format!(
"model files are present but {} is missing; refusing to load \
unverified weights (SPEC §6.1)",
Slash(&manifest_path)
),
};
}
};
match StaticEmbedder::load(&paths, &manifest) {
Ok(e) => EmbedderState::Loaded {
embedder: Arc::new(e),
manifest,
paths,
},
Err(e) => EmbedderState::Absent {
reason: e.to_string(),
},
}
}
fn embedder_summary(&self) -> EmbedderSummary {
match self.embedder() {
EmbedderState::Loaded { embedder, .. } => EmbedderSummary {
loaded: true,
profile_id: Some(embedder.profile_id().to_string()),
dim: Some(embedder.dim()),
reason: None,
},
EmbedderState::Absent { reason } => EmbedderSummary {
loaded: false,
profile_id: None,
dim: None,
reason: Some(reason.clone()),
},
}
}
fn embedding_profile(&self) -> Option<EmbeddingProfile> {
match self.embedder() {
EmbedderState::Loaded {
embedder, manifest, ..
} => Some(EmbeddingProfile {
id: embedder.profile_id().to_string(),
dim: embedder.dim(),
model_hash: manifest.weights_blake3.clone(),
}),
EmbedderState::Absent { .. } => None,
}
}
fn declare_profile(&self, w: &Writers<'_>) -> Result<Option<ProfileChange>> {
let Some(profile) = self.embedding_profile() else {
return Ok(None);
};
let change = w.index.set_embedding_profile(&profile)?;
if change.changed {
w.policy.get().audit().record_raw(
&Actor::Cli.to_string(),
"index.embedding-profile-changed",
format!("embedding:{}", change.current.id),
json!({
"previous": change.previous,
"current": change.current,
"vectors_wiped": change.vectors_wiped,
}),
)?;
}
Ok(Some(change))
}
fn embed_blocks(&self, texts: &[&str]) -> Result<Option<Vec<Vec<f32>>>> {
match self.embedder() {
EmbedderState::Loaded { embedder, .. } => embedder.embed(texts).map(Some),
EmbedderState::Absent { .. } => Ok(None),
}
}
async fn llm(&self) -> LlmState {
let cached = self.llm.lock().ok().and_then(|g| g.clone());
if let Some(s) = cached {
return s;
}
let state = self.connect_llm().await;
if let Ok(mut g) = self.llm.lock() {
*g = Some(state.clone());
}
state
}
async fn connect_llm(&self) -> LlmState {
let inf = &self.config.inference;
let Some(model) = inf
.model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
else {
return LlmState::Absent(
"no inference model is configured (inference.model in cyberbrain.toml); the \
endpoint is never contacted without one"
.into(),
);
};
let cfg = LlmConfig {
base_url: inf.base_url.clone(),
model: model.to_string(),
timeout: std::time::Duration::from_millis(inf.timeout_ms),
allow_public_endpoint: inf.allow_public_endpoint,
allow_overlay_network: inf.allow_overlay_network,
..LlmConfig::default()
};
let audit: Arc<dyn cyberbrain_llm::AuditSink> = Arc::new(self.policy.audit().clone());
match LlmClient::connect(cfg, audit, self.gate.clone()).await {
Ok(c) => LlmState::Ready(Box::new(c)),
Err(e) => LlmState::Absent(e.to_string()),
}
}
pub fn scan(&self, opts: ScanOptions) -> Result<ScanReport> {
let started = Instant::now();
let w = self.writers(opts.dry_run);
let listing = self.store.list()?;
let mut report = ScanReport {
dry_run: opts.dry_run,
full: opts.full,
files_listed: listing.entries.len() + listing.skipped.len(),
indexed_new: 0,
reindexed_changed: 0,
revectorised: 0,
unchanged: 0,
touched_only: 0,
dropped_missing_file: Vec::new(),
skipped: listing
.skipped
.iter()
.map(|s| SkippedFile {
path: s.path.clone(),
reason: s.reason.clone(),
})
.collect(),
links_written_back: 0,
link_writeback_failed: Vec::new(),
oversized_blocks: Vec::new(),
embedder: EmbedderSummary::default(),
profile_change: None,
cleared: None,
index: lock_index(&self.index)?.stats()?,
audit_preview: Vec::new(),
elapsed_ms: 0,
};
if opts.full {
let cleared = w.index.clear()?;
w.policy.get().audit().record_raw(
&Actor::Cli.to_string(),
"index.cleared",
"index",
json!({ "erased": cleared, "dry_run": opts.dry_run }),
)?;
report.cleared = Some(cleared);
}
report.embedder = self.embedder_summary();
report.profile_change = self.declare_profile(&w)?;
let model_present = report.embedder.loaded;
let mut seen: HashSet<NoteId> = HashSet::new();
for entry in &listing.entries {
let mut note = match self.store.read_path(&entry.path) {
Ok(n) => n,
Err(e) => {
report.skipped.push(SkippedFile {
path: entry.path.clone(),
reason: match &e {
Error::Frontmatter { reason, .. } => {
format!("unreadable frontmatter: {reason}")
}
other => other.to_string(),
},
});
continue;
}
};
if !seen.insert(note.front.id) {
report.skipped.push(SkippedFile {
path: entry.path.clone(),
reason: format!(
"id {} is already used by another note in this tree; ids are unique",
note.front.id
),
});
continue;
}
let targets = link_targets(¬e.body);
if targets != note.front.links {
note.front.links = targets;
match w.notes.write(¬e) {
Ok(_) => report.links_written_back += 1,
Err(e) => report
.link_writeback_failed
.push(format!("{}: {e}", note.front.name)),
}
}
let hash = content_hash(¬e);
let existing = lock_index(&self.index)?.note(¬e.front.id)?;
enum Decision {
New,
Changed,
NeedsVectors,
TouchedOnly,
Unchanged,
}
let decision = match &existing {
None => Decision::New,
Some(rec) if rec.hash != hash => Decision::Changed,
Some(rec) if model_present && rec.vector_count < rec.block_count => {
Decision::NeedsVectors
}
Some(rec) if rec.stamp != stamp_of(¬e.path) => Decision::TouchedOnly,
Some(_) => Decision::Unchanged,
};
match decision {
Decision::Unchanged => {
report.unchanged += 1;
continue;
}
Decision::TouchedOnly => {
report.touched_only += 1;
continue;
}
_ => {}
}
let (blocks, oversized) = blocks_of(¬e, MAX_BLOCK_TOKENS);
for o in oversized {
report.oversized_blocks.push(OversizedBlock {
note: note.front.name.clone(),
block_idx: o.block_idx,
approx_tokens: o.approx_tokens,
reason: oversized_reason(o.reason),
});
}
let texts: Vec<&str> = blocks.iter().map(|b| b.text.as_str()).collect();
let vectors = match self.embed_blocks(&texts) {
Ok(v) => v,
Err(e) => {
report.skipped.push(SkippedFile {
path: entry.path.clone(),
reason: format!("embedding failed: {e}"),
});
continue;
}
};
match w.index.upsert_note(¬e, &blocks, vectors.as_deref()) {
Ok(_) => match decision {
Decision::New => report.indexed_new += 1,
Decision::Changed => report.reindexed_changed += 1,
Decision::NeedsVectors => report.revectorised += 1,
_ => unreachable!(),
},
Err(e) => report.skipped.push(SkippedFile {
path: entry.path.clone(),
reason: format!("the index refused it: {e}"),
}),
}
}
let known = lock_index(&self.index)?.notes()?;
for rec in known {
if seen.contains(&rec.front.id) {
continue;
}
let erased = w.index.delete_note(&rec.front.id)?;
w.policy.get().audit().record_raw(
&Actor::Cli.to_string(),
"index.note-dropped",
format!("note:{}", erased.id),
json!({
"name": erased.name,
"ring": erased.ring,
"path": erased.path,
"reason": "file missing at scan",
"counts": erased.counts,
"dry_run": opts.dry_run,
}),
)?;
report.dropped_missing_file.push(erased.name);
}
report.index = lock_index(&self.index)?.stats()?;
report.audit_preview = w.policy.preview();
report.elapsed_ms = started.elapsed().as_millis();
Ok(report)
}
fn staleness(&self) -> Result<(usize, usize, usize, Vec<SkippedFile>)> {
let listing = self.store.list()?;
let ix = lock_index(&self.index)?;
let mut not_indexed = 0;
let mut changed = 0;
let mut unreadable = Vec::new();
let mut seen = HashSet::new();
for e in &listing.entries {
match self.store.read_path(&e.path) {
Ok(n) => {
seen.insert(n.front.id);
let mut probe = n.clone();
probe.front.links = link_targets(&n.body);
match ix.note(&n.front.id)? {
None => not_indexed += 1,
Some(rec) if rec.hash != content_hash(&probe) => changed += 1,
Some(_) => {}
}
}
Err(err) => unreadable.push(SkippedFile {
path: e.path.clone(),
reason: err.to_string(),
}),
}
}
let gone = ix
.notes()?
.into_iter()
.filter(|r| !seen.contains(&r.front.id))
.count();
Ok((not_indexed, changed, gone, unreadable))
}
pub async fn recall(&self, query: &str, req: &RecallRequest) -> Result<RecallResult> {
let r = &self.config.retrieval;
let opts = RecallOptions {
n: req.n.unwrap_or(r.n),
k_lex: r.k_lex,
k_sem: r.k_sem,
ring: req.ring,
min_cosine: 0.0,
};
let embedder_state = self.embedder();
let mut result = {
let ix = lock_index(&self.index)?;
let embedder: Option<&dyn Embedder> = match embedder_state {
EmbedderState::Loaded { embedder, .. } => Some(embedder.as_ref()),
EmbedderState::Absent { .. } => None,
};
ix.recall(query, embedder, &opts)?
};
if let EmbedderState::Absent { reason } = embedder_state {
result
.caveats
.push(format!("embedder not loaded: {reason}"));
}
if result.hits.len() >= 2 {
match self.llm().await {
LlmState::Ready(client) => {
let budget_ms = self.config.inference.contradiction_budget_ms;
let last = match hostload::LoadLog::new(&self.root)
.last_of(&[TASK_CONTRADICTION, TASK_CONTRADICTION_ABANDONED])
{
None => LastCheck::Unknown,
Some(r) if stale(&r.at) => LastCheck::Unknown,
Some(r) if r.task == TASK_CONTRADICTION_ABANDONED => LastCheck::Abandoned,
Some(r) => LastCheck::Completed { ms: r.wall_ms },
};
match plan_contradiction_check(budget_ms, last) {
CheckPlan::SkipMeasuredSlow { last_ms, budget_ms } => {
result.caveats.push(format!(
"contradiction check skipped: the last one took {}, over the {} \
budget (inference.contradiction_budget_ms); the hits are not \
checked against each other",
secs(last_ms),
secs(budget_ms)
));
}
CheckPlan::SkipAbandoned { budget_ms } => {
result.caveats.push(format!(
"contradiction check skipped: the last one was still running when \
its {} budget ran out (inference.contradiction_budget_ms); the \
hits are not checked against each other",
secs(budget_ms)
));
}
plan => {
let probe = self.load_probe();
let started = std::time::Instant::now();
let checked = match plan {
CheckPlan::Run(budget) => tokio::time::timeout(
budget,
cyberbrain_llm::tasks::find_conflicts(&client, &result.hits),
)
.await
.ok(),
_ => Some(
cyberbrain_llm::tasks::find_conflicts(&client, &result.hits)
.await,
),
};
match checked {
Some((conflicts, caveats)) => {
self.record_load(TASK_CONTRADICTION, probe, started.elapsed());
result.conflicts = conflicts;
result.caveats.extend(caveats);
}
None => {
self.record_load(
TASK_CONTRADICTION_ABANDONED,
probe,
started.elapsed(),
);
result.caveats.push(format!(
"contradiction check gave up after {} \
(inference.contradiction_budget_ms); the hits are not \
checked against each other",
secs(budget_ms)
));
}
}
}
}
}
LlmState::Absent(reason) => result
.caveats
.push(format!("contradiction check skipped: {reason}")),
}
} else {
result.caveats.push(
"contradiction check skipped: fewer than two hits, nothing to compare".into(),
);
}
self.record_recall_usage(&result);
Ok(result)
}
fn record_recall_usage(&self, result: &RecallResult) {
if result.hits.is_empty() {
return;
}
let cited: std::collections::HashSet<&str> =
result.hits.iter().map(|h| h.citation.as_str()).collect();
let notes: std::collections::BTreeSet<&NoteId> =
result.hits.iter().map(|h| &h.note_id).collect();
let Ok(ix) = lock_index(&self.index) else {
return;
};
let (mut returned, mut full) = (0u64, 0u64);
for id in ¬es {
let Ok(blocks) = ix.blocks_of(id) else {
return;
};
for b in blocks {
full += u64::from(b.token_count);
if cited.contains(b.citation.to_string().as_str()) {
returned += u64::from(b.token_count);
}
}
}
drop(ix);
self.usage().append(&usage::UsageRow {
at: usage::now(),
op: "recall".into(),
unit: "tokens".into(),
returned,
full,
hits: result.hits.len() as u64,
sources: notes.len() as u64,
});
}
fn load_probe(&self) -> (Option<hostload::HostSample>, Option<hostload::CgroupSample>) {
(hostload::read_host(), self.cgroup_sample())
}
fn cgroup_sample(&self) -> Option<hostload::CgroupSample> {
let dir = self.config.inference.load_cgroup.as_ref()?;
hostload::read_cgroup(Path::new(dir))
}
fn record_load(
&self,
task: &str,
before: (Option<hostload::HostSample>, Option<hostload::CgroupSample>),
wall: std::time::Duration,
) {
let after = (hostload::read_host(), self.cgroup_sample());
let row = hostload::row(task, wall, (before.0, after.0), (before.1, after.1));
hostload::LoadLog::new(&self.root).append(&row);
}
pub fn usage_by_day(&self, days: usize) -> Vec<usage::DayBucket> {
let axis = usage::day_axis(days);
let mut by_date: std::collections::BTreeMap<String, usage::DayBucket> = axis
.iter()
.map(|d| {
(
d.clone(),
usage::DayBucket {
date: d.clone(),
..Default::default()
},
)
})
.collect();
if let Ok(text) = std::fs::read_to_string(self.root.join("usage.jsonl")) {
for line in text.lines() {
let Ok(r) = serde_json::from_str::<usage::UsageRow>(line) else {
continue;
};
let Some(day) = usage::day_of(&r.at).and_then(|d| by_date.get_mut(d)) else {
continue;
};
let t = if r.op == "find" {
&mut day.find
} else {
&mut day.recall
};
t.ops += 1;
t.returned += r.returned;
t.full += r.full;
t.hits += r.hits;
}
}
let filter = AuditFilter {
action: Some("inference.call".into()),
..AuditFilter::default()
};
if let Ok(rows) = self.policy.audit().read(&filter) {
for r in rows {
let d = &r.detail;
let call = d.get("call").and_then(|v| v.as_str()).unwrap_or("");
if call != "chat-completion" && call != "chat-completion-stream" {
continue;
}
let ts = r.ts.to_string();
let Some(day) = usage::day_of(&ts).and_then(|d| by_date.get_mut(d)) else {
continue;
};
let num = |k: &str| d.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
day.calls += 1;
day.prompt_tokens += num("prompt_tokens");
day.cached_prompt_tokens += num("cached_prompt_tokens");
day.completion_tokens += num("completion_tokens");
}
}
let mut cores: std::collections::BTreeMap<String, (f64, u64, f64, u64)> =
std::collections::BTreeMap::new();
if let Ok(text) = std::fs::read_to_string(self.root.join("load.jsonl")) {
for line in text.lines() {
let Ok(r) = serde_json::from_str::<hostload::LoadRow>(line) else {
continue;
};
let Some(date) = usage::day_of(&r.at).map(str::to_owned) else {
continue;
};
if let Some(day) = by_date.get_mut(&date) {
day.wall_ms += r.wall_ms;
}
let e = cores.entry(date).or_insert((0.0, 0, 0.0, 0));
if let Some(c) = r.endpoint_cores {
e.0 += c;
e.1 += 1;
}
if let Some(c) = r.machine_cores {
e.2 += c;
e.3 += 1;
}
}
}
for (date, (ep, epn, ma, man)) in cores {
let Some(day) = by_date.get_mut(&date) else {
continue;
};
if epn > 0 {
day.endpoint_cores = Some(ep / epn as f64);
}
if man > 0 {
day.machine_cores = Some(ma / man as f64);
}
}
by_date.into_values().collect()
}
pub fn load_summary(&self) -> hostload::LoadSummary {
hostload::LoadLog::new(&self.root).summary()
}
pub async fn loaded_models(&self) -> Option<Vec<cyberbrain_llm::LoadedModel>> {
match self.llm().await {
LlmState::Ready(client) => client.loaded_models().await,
LlmState::Absent(_) => None,
}
}
fn usage(&self) -> usage::UsageLog {
usage::UsageLog::new(&self.root)
}
pub fn usage_summary(&self) -> usage::UsageSummary {
self.usage().summary()
}
pub fn inference_usage(&self) -> usage::InferenceUsage {
let filter = AuditFilter {
action: Some("inference.call".into()),
..AuditFilter::default()
};
let Ok(rows) = self.policy.audit().read(&filter) else {
return usage::InferenceUsage::default();
};
let mut out = usage::InferenceUsage::default();
for r in rows {
let d = &r.detail;
let call = d.get("call").and_then(|v| v.as_str()).unwrap_or("");
if call != "chat-completion" && call != "chat-completion-stream" {
continue;
}
let task = d
.get("task")
.and_then(|v| v.as_str())
.unwrap_or("unnamed")
.to_string();
let e = out.tasks.entry(task).or_default();
let num = |k: &str| d.get(k).and_then(|v| v.as_u64());
e.calls += 1;
e.elapsed_ms += num("elapsed_ms").unwrap_or(0);
if d.get("outcome").and_then(|v| v.as_str()) != Some("ok") {
e.failed += 1;
}
match (num("prompt_tokens"), num("completion_tokens")) {
(Some(p), c) => {
e.prompt_tokens += p;
e.completion_tokens += c.unwrap_or(0);
match num("cached_prompt_tokens") {
Some(c) => e.cached_prompt_tokens += c,
None => e.calls_without_cache_report += 1,
}
}
_ => e.calls_without_counts += 1,
}
if out.first.is_none() {
out.first = Some(r.ts.to_string());
}
out.last = Some(r.ts.to_string());
}
out
}
pub fn recall_id(&self, citation: &str) -> Result<Expanded> {
let cit: Citation = citation.parse()?;
let (block, rec) = lock_index(&self.index)?.resolve(&cit)?.ok_or_else(|| {
Error::NoSuchNote(format!(
"citation {cit} (not in the index; run `cyberbrain scan` if the note exists)"
))
})?;
let note = self.note_view_at(&rec.path)?;
Ok(Expanded {
citation: cit.to_string(),
block: BlockView {
citation: block.citation.to_string(),
idx: block.idx,
text: block.text,
token_count: block.token_count,
},
note,
})
}
fn note_view_at(&self, path: &Path) -> Result<NoteView> {
let n = self.store.read_path(path)?;
let blocks = lock_index(&self.index)?
.blocks_of(&n.front.id)?
.into_iter()
.map(|b| b.citation.to_string())
.collect();
Ok(NoteView {
kind: kind_name(n.front.kind),
front: n.front,
body: n.body,
path: n.path,
blocks,
})
}
fn resolve_target(&self, target: &str) -> Result<Note> {
if let Ok(n) = self.store.read(target) {
return Ok(n);
}
if let Ok(id) = NoteId::from_string(target) {
if let Some(rec) = lock_index(&self.index)?.note(&id)? {
return self.store.read_path(&rec.path);
}
return self.store.read_by_id(id);
}
Err(Error::NoSuchNote(target.to_string()))
}
pub fn export(&self, target: &str) -> Result<NoteView> {
let n = self.resolve_target(target)?;
self.note_view_at(&n.path)
}
pub fn write(&self, req: WriteRequest) -> Result<WriteOutcome> {
let name = req.name.trim().to_string();
frontmatter::validate_name(&name).map_err(|why| Error::Frontmatter {
path: PathBuf::from(format!("{name}.md")),
reason: format!("name `{name}`: {why}"),
})?;
if let Some(r) = &req.retention {
frontmatter::validate_retention(r).map_err(|why| Error::Frontmatter {
path: PathBuf::from(format!("{name}.md")),
reason: format!("retention `{r}`: {why}"),
})?;
}
let w = self.writers(req.dry_run);
let policy = w.policy.get();
let existing = match self.store.read(&name) {
Ok(n) => Some(n),
Err(Error::NoSuchNote(_)) => None,
Err(e) => return Err(e),
};
if let (Some(cur), Some(expected)) = (&existing, req.expected_updated)
&& cur.front.updated != expected
{
return Ok(WriteOutcome::Conflict {
name,
current_updated: cur.front.updated,
});
}
let (body, pii, redacted) = match policy.check_write(&name, &req.body)? {
WriteVerdict::Proceed { pii, .. } => (req.body.clone(), pii, 0),
WriteVerdict::Held { findings } => {
let choice = req.choice.or(if req.force {
Some(OperatorChoice::ProceedFlagged)
} else {
None
});
match choice {
None => {
return Ok(WriteOutcome::Held {
rendered: cyberbrain_policy::write_gate::render_hold(
&req.body, &findings,
),
name,
findings,
});
}
Some(c) => {
let r = policy.resolve_hold(&name, &req.body, &findings, c)?;
(r.body, r.pii, r.redacted)
}
}
}
};
let now = jiff::Timestamp::now();
let mut tags: Vec<String> = Vec::new();
for t in req.tags {
let t = t.trim().to_string();
if !t.is_empty() && !tags.contains(&t) {
tags.push(t);
}
}
let front = Frontmatter {
id: existing
.as_ref()
.map(|n| n.front.id)
.unwrap_or_else(NoteId::generate),
name: name.clone(),
ring: req.ring,
kind: req.kind,
created: existing.as_ref().map(|n| n.front.created).unwrap_or(now),
updated: now,
tags,
links: link_targets(&body),
retention: req.retention,
pii,
};
let note = Note {
front,
body,
path: PathBuf::new(),
};
let bytes = frontmatter::render(¬e.front, ¬e.body)?.len();
let path = w.notes.write(¬e)?;
let note = Note { path, ..note };
policy.record_write(¬e.front, bytes)?;
let (blocks, _) = blocks_of(¬e, MAX_BLOCK_TOKENS);
let texts: Vec<&str> = blocks.iter().map(|b| b.text.as_str()).collect();
self.declare_profile(&w)?;
let vectors = self.embed_blocks(&texts)?;
let outcome = w.index.upsert_note(¬e, &blocks, vectors.as_deref())?;
Ok(WriteOutcome::Written(WrittenNote {
id: note.front.id,
name,
ring: note.front.ring,
kind: kind_name(note.front.kind),
path: note.path,
bytes,
created: existing.is_none(),
updated: now,
pii,
redacted,
blocks: outcome.blocks,
vectors: outcome.vectors,
links: outcome.links,
embedder_reason: self.embedder_summary().reason,
dry_run: req.dry_run,
audit_preview: w.policy.preview(),
}))
}
pub fn write_manifest(&self, dir: &Path) -> Result<ManifestReport> {
let paths = cyberbrain_embed::ModelPaths::in_dir(dir);
for (what, path) in [
("model.safetensors", &paths.weights),
("tokenizer.json", &paths.tokenizer),
] {
if !path.is_file() {
return Err(Error::Config(format!(
"no {what} in {}; put the model2vec artefact there first",
Slash(dir)
)));
}
}
let manifest = cyberbrain_embed::ArtefactManifest {
weights_blake3: cyberbrain_embed::hash_file(&paths.weights)?,
tokenizer_blake3: cyberbrain_embed::hash_file(&paths.tokenizer)?,
};
let path = dir.join("manifest.json");
let text = serde_json::to_string_pretty(&manifest)
.map_err(|e| Error::Index(format!("the manifest does not serialise: {e}")))?
+ "\n";
cyberbrain_core::store::write_atomic(&path, text.as_bytes())?;
Ok(ManifestReport {
path,
weights_blake3: manifest.weights_blake3,
tokenizer_blake3: manifest.tokenizer_blake3,
})
}
pub fn propose(&self, req: WriteRequest, who: &str) -> Result<Proposed> {
let name = req.name.trim().to_string();
frontmatter::validate_name(&name).map_err(|why| Error::Frontmatter {
path: PathBuf::from(format!("{name}.md")),
reason: format!("name `{name}`: {why}"),
})?;
if let Some(r) = &req.retention {
frontmatter::validate_retention(r).map_err(|why| Error::Frontmatter {
path: PathBuf::from(format!("{name}.md")),
reason: format!("retention `{r}`: {why}"),
})?;
}
if self.store.read_proposal(&name).is_ok() {
return Err(Error::Config(format!(
"a proposal named {name} is already waiting; `cyberbrain review {name} --reject` \
it first, or propose under another name"
)));
}
let w = self.writers(req.dry_run);
let policy = w.policy.get();
let (body, pii, redacted) = match policy.check_write(&name, &req.body)? {
WriteVerdict::Proceed { pii, .. } => (req.body.clone(), pii, 0),
WriteVerdict::Held { findings } => {
let choice = req.choice.or(if req.force {
Some(OperatorChoice::ProceedFlagged)
} else {
None
});
match choice {
None => {
return Ok(Proposed::Held {
rendered: cyberbrain_policy::write_gate::render_hold(
&req.body, &findings,
),
name,
findings,
});
}
Some(c) => {
let r = policy.resolve_hold(&name, &req.body, &findings, c)?;
(r.body, r.pii, r.redacted)
}
}
}
};
let now = jiff::Timestamp::now();
let mut tags: Vec<String> = Vec::new();
for t in req.tags {
let t = t.trim().to_string();
if !t.is_empty() && !tags.contains(&t) {
tags.push(t);
}
}
let replaces = match self.store.read(&name) {
Ok(n) => Some(n.front.updated),
Err(Error::NoSuchNote(_)) => None,
Err(e) => return Err(e),
};
let note = Note {
front: Frontmatter {
id: NoteId::generate(),
name: name.clone(),
ring: req.ring,
kind: req.kind,
created: now,
updated: now,
tags,
links: link_targets(&body),
retention: req.retention,
pii,
},
body,
path: PathBuf::new(),
};
let bytes = frontmatter::render(¬e.front, ¬e.body)?.len();
let path = if req.dry_run {
self.store.proposal_path(&name)?
} else {
self.store.write_proposal(¬e)?
};
policy.audit().record(
&self.actor,
AuditAction::NoteProposed,
format!("note:{name}"),
serde_json::json!({
"by": who,
"ring": req.ring.as_u8(),
"kind": kind_name(req.kind),
"bytes": bytes,
"pii": pii,
"changes_existing": replaces.is_some(),
"dry_run": req.dry_run,
}),
)?;
Ok(Proposed::Written(ProposeReport {
name,
ring: note.front.ring,
kind: kind_name(note.front.kind),
path,
proposed_by: who.to_string(),
bytes,
pii,
redacted,
changes_existing: replaces.is_some(),
dry_run: req.dry_run,
audit_preview: w.policy.preview(),
}))
}
pub fn proposals(&self) -> Result<Vec<ProposalSummary>> {
let mut out = Vec::new();
for note in self.store.list_proposals()? {
let name = note.front.name.clone();
out.push(ProposalSummary {
proposed_by: self.proposer_of(&name)?,
changes_existing: self.store.read(&name).is_ok(),
name,
ring: note.front.ring,
kind: kind_name(note.front.kind),
created: note.front.created,
path: note.path,
});
}
Ok(out)
}
fn proposer_of(&self, name: &str) -> Result<Option<String>> {
let filter = AuditFilter {
subject: Some(format!("note:{name}")),
..AuditFilter::default()
};
let rows = self.policy.audit().read(&filter)?;
let last = rows.iter().rev().find(|e| {
e.action == AuditAction::NoteProposed.as_str()
|| e.action == AuditAction::NoteProposalAccepted.as_str()
|| e.action == AuditAction::NoteProposalRejected.as_str()
});
Ok(match last {
Some(e) if e.action == AuditAction::NoteProposed.as_str() => e
.detail
.get("by")
.and_then(|v| v.as_str())
.map(str::to_string),
_ => None,
})
}
pub fn review(&self, req: ReviewRequest) -> Result<ReviewReport> {
let note = self.store.read_proposal(&req.name)?;
let name = note.front.name.clone();
let Some(proposer) = self.proposer_of(&name)? else {
return Err(Error::Config(format!(
"the audit log has no record of {name} being proposed, so there is nobody to \
check this against. A file that appeared in proposals/ without going through \
`cyberbrain propose` is not a proposal; delete it or propose it properly"
)));
};
if proposer == req.by {
return Err(Error::PolicyRefusal {
profile: "review".to_string(),
reason: format!(
"a proposal cannot be reviewed by the person who made it. {name} was \
proposed by {proposer}"
),
});
}
let w = self.writers(req.dry_run);
let policy = w.policy.get();
if !req.accept {
let reason = req.reason.trim();
if reason.is_empty() {
return Err(Error::Config(
"rejecting needs a reason: it is the only place the proposer will look".into(),
));
}
if !req.dry_run {
self.store.remove_proposal(&name)?;
}
policy.audit().record(
&self.actor,
AuditAction::NoteProposalRejected,
format!("note:{name}"),
serde_json::json!({
"by": req.by, "proposed_by": proposer, "reason": reason,
"dry_run": req.dry_run,
}),
)?;
return Ok(ReviewReport {
name,
accepted: false,
by: req.by,
proposed_by: proposer,
reason: Some(reason.to_string()),
path: None,
blocks: 0,
vectors: 0,
dry_run: req.dry_run,
audit_preview: w.policy.preview(),
});
}
if let Ok(existing) = self.store.read(&name)
&& existing.front.updated > note.front.created
&& !req.force
{
return Err(Error::Config(format!(
"{name} changed after this was proposed ({} against {}); accepting would \
overwrite the newer text. Read both, then `--force` if the proposal is \
still right",
existing.front.updated, note.front.created
)));
}
let pii = match policy.check_write(&name, ¬e.body)? {
WriteVerdict::Proceed { pii, .. } => pii,
WriteVerdict::Held { findings } => {
return Err(Error::PolicyRefusal {
profile: "pii".to_string(),
reason: format!(
"{name} holds {} possible personal data item(s) and cannot be accepted \
as it stands. Reject it with a reason; the proposer resolves it and \
proposes again",
findings.len()
),
});
}
};
let now = jiff::Timestamp::now();
let existing = self.store.read(&name).ok();
let accepted = Note {
front: Frontmatter {
id: existing
.as_ref()
.map(|n| n.front.id)
.unwrap_or_else(NoteId::generate),
created: existing.as_ref().map(|n| n.front.created).unwrap_or(now),
updated: now,
pii,
..note.front.clone()
},
body: note.body.clone(),
path: PathBuf::new(),
};
let bytes = frontmatter::render(&accepted.front, &accepted.body)?.len();
let ring = accepted.front.ring;
let (path, blocks, vectors) = if req.dry_run {
(self.store.note_path(ring, &name)?, 0, 0)
} else {
let path = w.notes.write(&accepted)?;
let accepted = Note {
path: path.clone(),
..accepted
};
policy.record_write(&accepted.front, bytes)?;
let (blocks, _) = blocks_of(&accepted, MAX_BLOCK_TOKENS);
let texts: Vec<&str> = blocks.iter().map(|b| b.text.as_str()).collect();
self.declare_profile(&w)?;
let vectors = self.embed_blocks(&texts)?;
let outcome = w
.index
.upsert_note(&accepted, &blocks, vectors.as_deref())?;
self.store.remove_proposal(&name)?;
(path, outcome.blocks, outcome.vectors)
};
policy.audit().record(
&self.actor,
AuditAction::NoteProposalAccepted,
format!("note:{name}"),
serde_json::json!({
"by": req.by, "proposed_by": proposer,
"ring": ring.as_u8(), "bytes": bytes,
"dry_run": req.dry_run,
}),
)?;
Ok(ReviewReport {
name,
accepted: true,
by: req.by,
proposed_by: proposer,
reason: None,
path: Some(path),
blocks,
vectors,
dry_run: req.dry_run,
audit_preview: w.policy.preview(),
})
}
pub fn forget(&self, target: &str, dry_run: bool) -> Result<ErasureReport> {
let req = self.erase_request(target, EraseReason::OperatorForget, dry_run)?;
let w = self.writers(dry_run);
let mut eraser = StoreEraser {
notes: w.notes.as_ref(),
index: w.index.as_ref(),
};
let mut report = w.policy.get().forget(&mut eraser, &req)?;
if dry_run {
for a in w.policy.preview() {
report
.notes
.push(format!("audit row a real run would append: {a}"));
}
}
Ok(report)
}
fn erase_request(
&self,
target: &str,
reason: EraseReason,
dry_run: bool,
) -> Result<EraseRequest> {
let (id, name, ring, path) = match self.resolve_target(target) {
Ok(n) => (n.front.id, n.front.name, n.front.ring, n.path),
Err(Error::NoSuchNote(_)) => {
let ix = lock_index(&self.index)?;
let rec = match ix.note_by_name(target)? {
Some(r) => Some(r),
None => match NoteId::from_string(target) {
Ok(id) => ix.note(&id)?,
Err(_) => None,
},
};
let rec = rec.ok_or_else(|| Error::NoSuchNote(target.to_string()))?;
(rec.front.id, rec.front.name, rec.front.ring, rec.path)
}
Err(e) => return Err(e),
};
Ok(EraseRequest {
note_id: id,
name,
ring,
path,
reason,
dry_run,
})
}
pub fn doctor(&self) -> Result<DoctorReport> {
let mut findings = Vec::new();
let mut checks = Vec::new();
let mut push = |severity: &'static str, check: &'static str, detail: String| {
findings.push(DoctorFinding {
severity,
check,
detail,
})
};
checks.push("notes tree");
let listing = self.store.list()?;
for s in &listing.skipped {
push(
"warning",
"notes tree",
format!("{}: {}", Slash(&s.path), s.reason),
);
}
checks.push("stale index");
let (not_indexed, changed, gone, unreadable) = self.staleness()?;
for u in unreadable {
push(
"error",
"unreadable note",
format!("{}: {}", Slash(&u.path), u.reason),
);
}
if not_indexed + changed + gone > 0 {
push(
"warning",
"stale index",
format!(
"{not_indexed} notes not indexed, {changed} changed on disk since the last \
scan, {gone} indexed notes whose file is gone; run `cyberbrain scan`"
),
);
}
checks.push("dangling links");
checks.push("unresolvable links");
{
let ix = lock_index(&self.index)?;
for l in ix.dangling_links()? {
let from = ix
.note(&l.from_note)?
.map(|r| r.front.name)
.unwrap_or_else(|| l.from_note.to_string());
match cyberbrain_core::validate_name(&l.to_name) {
Ok(()) => push(
"warning",
"dangling links",
format!(
"{from} links to [[{}]] which does not exist yet (valid name: it names intent)",
l.to_name
),
),
Err(reason) => {
let normalised = normalise_link_target(&l.to_name);
let hint = match ix.note_by_name(&normalised)? {
Some(_) => format!("; did you mean [[{normalised}]]?"),
None => String::new(),
};
push(
"warning",
"unresolvable links",
format!(
"{from} links to [[{}]], which can never resolve: a note name {reason}{hint}",
l.to_name
),
)
}
}
}
}
checks.push("ring cap");
let resident = self.store.resident_tokens()?;
let cap = self.store.resident_cap();
if resident > cap {
push(
"error",
"ring cap",
format!(
"rings 0+1 hold ~{resident} tokens, over the cap of {cap}; a hand edit crossed it"
),
);
} else if resident * 10 >= cap * 8 {
push(
"warning",
"ring cap",
format!(
"rings 0+1 hold ~{resident} of {cap} tokens ({}%)",
resident * 100 / cap
),
);
}
checks.push("index integrity");
for p in lock_index(&self.index)?.integrity()? {
push("error", "index integrity", p);
}
checks.push("embedding profile");
let stored = lock_index(&self.index)?.embedding_profile()?;
match (stored, self.embedder()) {
(Some(p), EmbedderState::Loaded { embedder, .. }) => {
if let Err(e) = lock_index(&self.index)?.check_embedder(embedder.as_ref()) {
let _ = p;
push("error", "embedding profile", e.to_string());
}
}
(Some(p), EmbedderState::Absent { reason }) => push(
"warning",
"embedding profile",
format!(
"index vectors come from {} but no model is loaded ({reason}); semantic search is off",
p.id
),
),
(None, EmbedderState::Loaded { .. }) => {
if lock_index(&self.index)?.stats()?.blocks > 0 {
push(
"warning",
"embedding profile",
"a model is present but the index holds no vectors; run `cyberbrain scan`"
.into(),
);
}
}
(None, EmbedderState::Absent { .. }) => {}
}
checks.push("audit chain");
if let Err(e) = self.policy.verify_audit() {
push("error", "audit chain", e.to_string());
}
checks.push("retention");
let (queue, _) = self.retention_queue()?;
for i in &queue.items {
if let cyberbrain_policy::RetentionStatus::Invalid { reason } = &i.status {
push("warning", "retention", format!("{}: {reason}", i.name));
}
}
if queue.due > 0 {
push(
"warning",
"retention",
format!(
"{} note(s) past their retention; nothing expires by itself, run `cyberbrain policy retention`",
queue.due
),
);
}
Ok(DoctorReport {
clean: findings.is_empty(),
checks_run: checks,
findings,
})
}
pub async fn status(&self) -> Result<StatusReport> {
let listing = self.store.list()?;
let mut per_ring = [0usize; 5];
for e in &listing.entries {
per_ring[e.ring.as_u8() as usize] += 1;
}
let index = lock_index(&self.index)?.stats()?;
let (not_indexed, changed, gone, _) = self.staleness()?;
let embedder = self.embedder_summary();
let matches_index = match (&index.embedding, self.embedder()) {
(Some(_), EmbedderState::Loaded { embedder, .. }) => Some(
lock_index(&self.index)?
.check_embedder(embedder.as_ref())
.is_ok(),
),
_ => None,
};
let model_dir = self.config.model_dir();
let inf = &self.config.inference;
let inference = match self.llm().await {
LlmState::Ready(c) => {
let probe = c.probe().await;
InferenceStatus {
endpoint: inf.base_url.clone(),
model: inf.model.clone(),
state: if probe.reachable {
"reachable".into()
} else {
"configured but unreachable".into()
},
probe: Some(probe),
}
}
LlmState::Absent(reason) => InferenceStatus {
endpoint: inf.base_url.clone(),
model: inf.model.clone(),
state: format!("not in use: {reason}"),
probe: None,
},
};
Ok(StatusReport {
store: self.root.clone(),
config: self.store.config_path(),
notes_on_disk: listing.entries.len(),
notes_per_ring: per_ring,
files_skipped: listing.skipped.len(),
resident_tokens: self.store.resident_tokens()?,
resident_cap: self.store.resident_cap(),
index_stale: not_indexed + changed + gone > 0,
index,
audit: AuditSummary {
path: self.root.join(AUDIT_DB_FILE),
rows: self.audit_sink.count()?,
schema_version: self.audit_sink.schema_version()?,
chain: self.policy.verify_audit().map_err(|e| e.to_string()),
},
embedding: EmbeddingStatus {
manifest_present: model_dir.join(MANIFEST_FILE).is_file(),
model_dir,
embedder,
index_profile: lock_index(&self.index)?.embedding_profile()?,
matches_index,
},
inference,
policy: self.policy.status(),
})
}
fn code_root(&self) -> PathBuf {
let is_default_store = self
.root
.file_name()
.is_some_and(|n| n == DEFAULT_STORE_DIR);
match (is_default_store, self.root.parent()) {
(true, Some(parent)) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
(true, Some(_)) => PathBuf::from("."),
_ => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
}
}
pub fn find(&self, symbol: &str, limit: usize) -> Result<FindReport> {
let opts = cyberbrain_code::FindOptions {
exclude: vec![self.root.clone()],
..cyberbrain_code::FindOptions::default()
};
let result = cyberbrain_code::find(&self.code_root(), symbol, limit, &opts)?;
let report = FindReport::from(result);
self.record_find_usage(&report);
Ok(report)
}
fn record_find_usage(&self, report: &FindReport) {
if report.hits.is_empty() {
return;
}
let mut returned = 0u64;
let mut full = 0u64;
let mut counted: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
for h in &report.hits {
returned += u64::from(h.end_line.saturating_sub(h.start_line)) + 1;
if counted.insert(h.path.as_str()) {
match std::fs::read_to_string(report.root.join(&h.path)) {
Ok(text) => full += text.lines().count() as u64,
Err(_) => {
counted.remove(h.path.as_str());
}
}
}
}
if full == 0 {
return;
}
self.usage().append(&usage::UsageRow {
at: usage::now(),
op: "find".into(),
unit: "lines".into(),
returned,
full,
hits: report.hits.len() as u64,
sources: counted.len() as u64,
});
}
pub fn policy_egress(&self) -> Vec<EgressEntry> {
self.policy.egress().register()
}
pub fn policy_obligations(&self) -> ObligationsView {
let profile = self.config.policy.profile;
ObligationsView {
profile,
law: profile.law().to_string(),
obligations: profile.obligations(),
}
}
pub fn enrol_with_hub(
&self,
hub_url: &str,
device: &str,
inference_url: Option<&str>,
) -> Result<Option<String>> {
let path = self.store.config_path();
let text = std::fs::read_to_string(&path).map_err(|e| Error::Io {
path: path.clone(),
source: e,
})?;
let mut updated = crate::hub::client::set_hub_in_config(&text, hub_url, device);
if let Some(url) = inference_url {
updated = crate::hub::client::set_inference_url(&updated, url);
}
cyberbrain_core::config::Config::parse(&updated, &self.root)
.map_err(|e| Error::Config(format!("enrolment would break the config file: {e}")))?;
std::fs::write(&path, updated).map_err(|e| Error::Io {
path: path.clone(),
source: e,
})?;
Ok(inference_url.map(str::to_owned))
}
pub fn hub_status(&self) -> Result<serde_json::Value> {
let Some(url) = self.config.hub.url.clone() else {
return Ok(serde_json::json!({ "enrolled": false }));
};
let filter = cyberbrain_policy::AuditFilter {
action: Some("egress.completed".into()),
contains: Some("audit-sync".into()),
..Default::default()
};
let last = self
.policy
.audit()
.read(&filter)
.ok()
.and_then(|rows| rows.last().map(|e| e.ts.to_string()));
Ok(serde_json::json!({
"enrolled": true,
"hub": url,
"device": self.config.hub.device,
"last_delivery": last,
}))
}
pub async fn push_to_hub(
&self,
since: Option<jiff::Timestamp>,
) -> Result<(serde_json::Value, i32)> {
use crate::hub::client::{self, Reply};
let hub_url = self.config.hub.url.clone().ok_or_else(|| {
Error::Config(
"this store is not enrolled with a hub; run `cyberbrain hub enrol <invitation>`"
.into(),
)
})?;
let token = client::token_for(&hub_url)?;
let version = env!("CARGO_PKG_VERSION");
let filter = cyberbrain_policy::AuditFilter {
since,
..Default::default()
};
let bundle = self.export_audit_bundle(&filter)?;
let egress = self.policy.egress();
let actor = cyberbrain_policy::Actor::Operator;
let pin = client::pin_for(&hub_url);
let reply = client::deliver(
egress,
&actor,
&hub_url,
&token,
pin.as_deref(),
version,
bundle,
)
.await?;
Ok(match reply {
Reply::Ok(d) => (
serde_json::json!({
"state": "delivered",
"accepted": d.accepted,
"total_rows": d.total_rows,
"hub": d.hub,
"message": format!(
"delivered {} new row(s) to {}; the hub now holds {}",
d.accepted, d.hub, d.total_rows
),
}),
0,
),
Reply::NotCollecting(m) => (
serde_json::json!({
"state": "not-collecting",
"message": format!("{m}\nNothing was lost; this store keeps its rows."),
}),
0,
),
Reply::Gap { expected } => (
serde_json::json!({
"state": "gap",
"expected_anchor": expected,
"message": format!(
"the hub is at {} and this delivery did not reach back that far. \
Send a wider period: `cyberbrain hub push` without --since covers \
everything.",
&expected[..expected.len().min(12)]
),
}),
0,
),
Reply::Refused { status, message } => (
serde_json::json!({
"state": "refused",
"status": status,
"message": format!("the hub refused the delivery ({status}): {message}"),
}),
1,
),
})
}
pub fn export_audit_bundle(&self, filter: &AuditFilter) -> Result<String> {
let tool = concat!("cyberbrain ", env!("CARGO_PKG_VERSION"));
self.policy.export_audit_bundle(filter, tool)
}
pub fn policy_audit(
&self,
filter: &AuditFilter,
verify: bool,
format: ExportFormat,
) -> Result<AuditView> {
let verified = verify.then(|| self.policy.verify_audit().map_err(|e| e.to_string()));
let rows = self.policy.audit().read(filter)?.len();
if rows == 0 && filter.action.is_some() {
let all = self.policy.audit().read(&AuditFilter::default())?;
if !all.is_empty() {
let wanted = filter.action.as_deref().unwrap_or_default();
let mut present: Vec<String> = all
.iter()
.map(|e| e.action.clone())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
present.sort();
if !present
.iter()
.any(|a| a == wanted || a.starts_with(&format!("{wanted}.")))
{
return Err(Error::Config(format!(
"no audit action named {wanted:?}; the log contains: {}. Refused rather than answered with an empty table: a missing name and a thing that never happened are different answers",
present.join(", ")
)));
}
}
}
let rendered = self.policy.export_audit(filter, format)?;
Ok(AuditView {
rows,
verified,
rendered,
})
}
pub fn policy_subject(&self, identifier: &str) -> Result<SubjectAccessReport> {
let source = IndexSubjectSource { index: &self.index };
self.policy.subject_access(&source, identifier)
}
fn retention_queue(&self) -> Result<(RetentionQueue, Vec<String>)> {
let listing = self.store.list()?;
let mut notes = Vec::new();
let mut unreadable = Vec::new();
for e in &listing.entries {
match self.store.read_path(&e.path) {
Ok(n) => notes.push((n.front, n.path)),
Err(err) => unreadable.push(format!("{}: {err}", Slash(&e.path))),
}
}
let queue = self
.policy
.retention_queue(notes.iter().map(|(f, p)| (f, p.as_path())));
Ok((queue, unreadable))
}
pub fn policy_retention(&self, apply: bool, dry_run: bool) -> Result<RetentionReport> {
let (queue, unreadable) = self.retention_queue()?;
let mut report = RetentionReport {
queue,
unreadable,
applied_run: apply,
dry_run,
applied: Vec::new(),
audit_preview: Vec::new(),
};
if !apply {
return Ok(report);
}
let w = self.writers(dry_run);
let mut eraser = StoreEraser {
notes: w.notes.as_ref(),
index: w.index.as_ref(),
};
for (item, result) in w
.policy
.get()
.apply_retention(&mut eraser, &report.queue, dry_run)
{
report.applied.push(RetentionOutcome {
name: item.name.clone(),
item,
result: result.map_err(|e| e.to_string()),
});
}
report.audit_preview = w.policy.preview();
Ok(report)
}
pub fn policy_model_card(&self) -> ModelCardReport {
let cards = self.policy.model_cards(&[self]);
let mut absent = Vec::new();
if let EmbedderState::Absent { reason } = self.embedder() {
absent.push(format!("embedding model: {reason}"));
}
if self.config.inference.model.is_none() {
absent.push(
"inference model: none configured (inference.model); the endpoint is not contacted"
.into(),
);
}
ModelCardReport { cards, absent }
}
pub fn policy_consent(&self, grant: bool) -> Result<ConsentReport> {
let path = self.store.config_path();
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
cyberbrain_core::config::DEFAULT_TOML.to_string()
}
Err(e) => return Err(Error::Io { path, source: e }),
};
let new_line = format!("model_download_consent = {grant}");
let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
let mut in_embedding = false;
let mut replaced = false;
let mut embedding_header: Option<usize> = None;
for (i, line) in lines.iter_mut().enumerate() {
let t = line.trim();
if t.starts_with('[') {
in_embedding = t == "[embedding]";
if in_embedding {
embedding_header = Some(i);
}
continue;
}
if in_embedding
&& !t.starts_with('#')
&& t.split('=').next().map(str::trim) == Some("model_download_consent")
{
*line = new_line.clone();
replaced = true;
}
}
if !replaced {
match embedding_header {
Some(i) => lines.insert(i + 1, new_line),
None => {
lines.push(String::new());
lines.push("[embedding]".into());
lines.push(new_line);
}
}
}
let mut out = lines.join("\n");
out.push('\n');
let parsed = Config::parse(&out, &path)?;
write_atomic(&path, out.as_bytes())?;
let mut warnings = Vec::new();
if parsed.embedding.model_source.is_none() {
warnings.push(
"embedding.model_source is unset, so nothing can be downloaded regardless of \
consent; the artefact has to be placed by hand"
.into(),
);
}
warnings.push(
"takes effect on the next command; this process keeps the register it started with"
.into(),
);
self.policy.audit().record_raw(
&self.actor.to_string(),
"consent.model-download",
"model-download",
json!({ "consent": grant, "model_source": parsed.embedding.model_source }),
)?;
Ok(ConsentReport {
path,
consent: grant,
model_source: parsed.embedding.model_source,
warnings,
})
}
}
impl ModelInventory for App {
fn model_cards(&self) -> Vec<ModelCard> {
let mut cards = Vec::new();
if let EmbedderState::Loaded {
embedder,
manifest,
paths,
} = self.embedder()
{
let info = embedder.info();
let dir = self.config.model_dir();
let mut c = ModelCard::new(
ModelRole::Embedding,
dir.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "model2vec".into()),
self.config
.embedding
.model_source
.clone()
.unwrap_or_else(|| "placed by hand (no model_source configured)".into()),
"static token embeddings for hybrid recall over the notes in this store",
);
c.blake3 = Some(manifest.weights_blake3.clone());
c.hash_verified = Some(true);
c.dimension = Some(info.dim);
c.pooling = Some(info.pooling.to_string());
c.format = Some(format!(
"model2vec: safetensors [{} x {}] {} + tokenizer.json (blake3 {})",
info.vocab_rows, info.dim, info.weights_dtype, manifest.tokenizer_blake3
));
c.size_bytes = std::fs::metadata(&paths.weights)
.ok()
.map(|m| m.len())
.and_then(|w| {
std::fs::metadata(&paths.tokenizer)
.ok()
.map(|t| w + t.len())
});
c.artefact_path = Some(dir);
c.limitations.push(
"static embeddings: no context, no word order; retrieval quality below a transformer".into(),
);
cards.push(c);
}
if let Some(model) = &self.config.inference.model {
let mut c = ModelCard::new(
ModelRole::Inference,
model.clone(),
self.config.inference.base_url.clone(),
"session summaries, contradiction checks between retrieved blocks, ring and tag suggestions, note supersession (SPEC §11)",
);
c.format = Some("OpenAI-compatible HTTP, operated by the deployer".into());
c.notes.push(
"licence, version and weights are the endpoint operator's; not read by this tool"
.into(),
);
cards.push(c);
}
cards
}
}
struct IndexSubjectSource<'a> {
index: &'a Mutex<Index>,
}
impl SubjectSource for IndexSubjectSource<'_> {
fn blocks_mentioning(&self, identifier: &Identifier) -> Result<Vec<SubjectBlock>> {
Ok(lock_index(self.index)?
.blocks_containing(identifier.raw())?
.into_iter()
.map(|(cit, name, text)| SubjectBlock {
citation: cit.to_string(),
note_id: None,
note_name: name,
ring: Some(cit.ring),
text,
})
.collect())
}
}
fn normalise_link_target(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for ch in name.chars() {
match ch {
'a'..='z' | '0'..='9' => out.push(ch),
'A'..='Z' => out.push(ch.to_ascii_lowercase()),
'_' | ' ' | '-' | '.' | '/' if !out.ends_with('-') => out.push('-'),
_ => {}
}
}
out.trim_matches('-').to_string()
}
#[cfg(test)]
mod contradiction_budget_tests {
use super::*;
#[test]
fn a_measured_slow_endpoint_is_not_asked_again() {
assert_eq!(
plan_contradiction_check(3_000, LastCheck::Completed { ms: 126_184 }),
CheckPlan::SkipMeasuredSlow {
last_ms: 126_184,
budget_ms: 3_000
}
);
}
#[test]
fn an_abandoned_check_is_not_retried_either() {
assert_eq!(
plan_contradiction_check(3_000, LastCheck::Abandoned),
CheckPlan::SkipAbandoned { budget_ms: 3_000 }
);
}
#[test]
fn a_fast_one_runs_under_the_budget() {
assert_eq!(
plan_contradiction_check(3_000, LastCheck::Completed { ms: 800 }),
CheckPlan::Run(std::time::Duration::from_millis(3_000))
);
assert_eq!(
plan_contradiction_check(3_000, LastCheck::Unknown),
CheckPlan::Run(std::time::Duration::from_millis(3_000))
);
}
#[test]
fn the_boundary_is_not_slow() {
assert_eq!(
plan_contradiction_check(3_000, LastCheck::Completed { ms: 3_000 }),
CheckPlan::Run(std::time::Duration::from_millis(3_000))
);
}
#[test]
fn a_measurement_expires_after_a_day() {
let now = jiff::Timestamp::now();
let fresh = (now - jiff::SignedDuration::from_hours(2)).to_string();
let old = (now - jiff::SignedDuration::from_hours(30)).to_string();
assert!(!stale(&fresh), "two hours old still counts");
assert!(stale(&old), "thirty hours old does not");
assert!(
stale("not a timestamp"),
"an unreadable stamp means try again"
);
}
#[test]
fn zero_means_wait_however_long_it_takes() {
assert_eq!(
plan_contradiction_check(0, LastCheck::Completed { ms: 126_184 }),
CheckPlan::RunUnbounded
);
assert_eq!(
plan_contradiction_check(0, LastCheck::Abandoned),
CheckPlan::RunUnbounded
);
}
#[test]
fn durations_read_like_durations() {
assert_eq!(secs(450), "450 ms");
assert_eq!(secs(3_000), "3.0 s");
assert_eq!(secs(126_184), "126 s");
}
}
#[cfg(test)]
mod find_tests {
use super::*;
#[test]
fn find_scans_the_project_and_not_the_store() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("proj");
std::fs::create_dir_all(project.join("src")).unwrap();
std::fs::write(
project.join("src/lib.rs"),
"/// Greets.\npub fn greet() -> &'static str {\n \"hi\"\n}\n\nfn main() {\n greet();\n}\n",
)
.unwrap();
let store = project.join(DEFAULT_STORE_DIR);
App::init(&store, &Actor::Cli).unwrap();
std::fs::write(store.join("notes/r2/greet.md"), "# greet\n\nnot code\n").unwrap();
let app = App::open(Some(&store), Actor::Cli).unwrap();
let r = app.find("greet", 10).unwrap();
assert_eq!(r.root, std::path::absolute(&project).unwrap());
assert_eq!(r.hits.len(), 1, "{:?}", r.hits);
let h = &r.hits[0];
assert_eq!(
(
h.path.as_str(),
h.kind,
h.language,
h.start_line,
h.line,
h.end_line,
h.matched
),
("src/lib.rs", "function", "rust", 1, 2, 4, "exact")
);
assert_eq!(
r.skipped.store_entries, 1,
"the store is excluded, not merely hidden"
);
assert_eq!(r.files_scanned, 1);
assert!(!r.truncated);
assert!(
r.caveats.iter().any(|c| c.contains(".cyberbrainignore")),
"no ignore file in the project: the report must say so: {:?}",
r.caveats
);
let v = serde_json::to_value(&r).unwrap();
for key in [
"symbol",
"name",
"scope",
"root",
"hits",
"matched_total",
"truncated",
"limit",
"files_scanned",
"bytes_scanned",
"definitions_indexed",
"skipped",
"ignore_files",
"caveats",
"elapsed_ms",
] {
assert!(v.get(key).is_some(), "FindReport lacks `{key}`");
}
for key in [
"path",
"start_line",
"end_line",
"line",
"kind",
"language",
"name",
"scope",
"matched",
"snippet",
] {
assert!(v["hits"][0].get(key).is_some(), "FindHit lacks `{key}`");
}
let e = app.find("", 10).unwrap_err();
assert_eq!(e.exit_code(), 1);
}
}
#[cfg(test)]
mod path_rendering_tests {
use super::*;
use cyberbrain_core::slash;
#[test]
fn doctor_findings_spell_paths_with_forward_slashes() {
let dir = tempfile::tempdir().unwrap();
let store = dir.path().join("store");
App::init(&store, &Actor::Operator).unwrap();
let stray = store.join("notes").join("r2").join("stray.txt");
std::fs::write(&stray, "not a note").unwrap();
let app = App::open(Some(&store), Actor::Operator).unwrap();
let r = app.doctor().unwrap();
let f = r
.findings
.iter()
.find(|f| f.check == "notes tree")
.unwrap_or_else(|| panic!("{r:?}"));
assert!(!f.detail.contains('\\'), "{}", f.detail);
assert!(f.detail.contains(&slash(&stray)), "{}", f.detail);
let v = serde_json::to_value(&r).unwrap();
for finding in v["findings"].as_array().unwrap() {
let detail = finding["detail"].as_str().unwrap();
assert!(!detail.contains('\\'), "{detail}");
}
}
}