use std::collections::HashMap;
use std::path::Path;
use omgbase_format::BlockKind;
use omgbase_format::text::{normalize_text, normalize_visible_text};
use omgbase_reconcile::Config;
use omgbase_search::EmbeddingProvider;
use omgbase_store::docs_ops::{canonical, retargeted_json};
use omgbase_store::links::inbound_links_to;
use omgbase_store::mutate_kernel::effects::{Admission, EffectExpect, Target as EffectTarget};
use omgbase_store::mutate_kernel::{At, ErrorCode, Parent};
use omgbase_store::{
ApplyOrigin, DocStore, Expect, FsDocStore, MutationError, Plan, PlanContext, PublicEffect,
QueryVector, Receipt, Retargeted, Store, sugar,
};
use omgbase_sync::{RealFileSystem, RepoRow};
use rusqlite::{OptionalExtension, params};
use serde_json::{Map, Value as Json, json};
use crate::arguments::{argument_error, validate_arguments};
use crate::dates;
use crate::error::{Result, SurfaceError};
use crate::fixes;
use crate::graph::{GraphArgs, graph_neighborhood};
use crate::history;
use crate::links;
use crate::names::VocabularyCache;
use crate::paths::{reference_keyed, reference_path, root_field, storage_path};
use crate::query::{QueryOptions, embed_phrases, query};
use crate::read::{self, Resolution, ResolvedRef};
use crate::reference::QUERY_SYNTAX;
use crate::select::{self, SelectRequest};
use crate::version::{BuildInfo, version_info};
pub const ACTOR: &str = "agent:mcp";
#[derive(Clone, Debug, PartialEq)]
pub struct ToolSpec {
pub name: &'static str,
pub description: &'static str,
pub input_schema: Json,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolOutcome {
pub body: Json,
pub is_error: bool,
}
enum WriteTarget {
Derived,
Fixed(Box<dyn DocStore>),
}
pub struct Surface {
store: Store,
default_repo: String,
provider: Option<Box<dyn EmbeddingProvider>>,
provider_name: Option<String>,
on_mutation: Option<Box<dyn FnMut()>>,
clock: Box<dyn FnMut() -> String>,
writes: WriteTarget,
config: Config,
actor: String,
build_info: BuildInfo,
preview: bool,
last_plan: Option<Plan>,
profile: Profile,
vocabulary: VocabularyCache,
}
fn bad_args(msg: impl Into<String>) -> SurfaceError {
SurfaceError::filter_invalid(msg, "arguments")
}
fn arg_str<'a>(args: &'a Json, key: &str) -> Option<&'a str> {
args.get(key).and_then(Json::as_str)
}
fn arg_string(args: &Json, key: &str) -> Result<String> {
arg_str(args, key)
.map(str::to_owned)
.ok_or_else(|| bad_args(format!("`{key}` must be a string")))
}
fn arg_i64(args: &Json, key: &str) -> Result<Option<i64>> {
match args.get(key) {
None | Some(Json::Null) => Ok(None),
Some(v) => v
.as_f64()
.filter(|n| n.fract() == 0.0)
.map(|n| Some(n as i64))
.ok_or_else(|| bad_args(format!("`{key}` must be an integer"))),
}
}
fn arg_usize(args: &Json, key: &str) -> Result<Option<usize>> {
Ok(arg_i64(args, key)?.map(|n| usize::try_from(n).unwrap_or(0)))
}
fn arg_bool(args: &Json, key: &str) -> Result<Option<bool>> {
match args.get(key) {
None | Some(Json::Null) => Ok(None),
Some(Json::Bool(b)) => Ok(Some(*b)),
Some(_) => Err(bad_args(format!("`{key}` must be a boolean"))),
}
}
fn arg_strings(args: &Json, key: &str) -> Result<Vec<String>> {
let Some(arr) = args.get(key).and_then(Json::as_array) else {
return Err(bad_args(format!("`{key}` must be an array of strings")));
};
arr.iter()
.map(|v| {
v.as_str()
.map(str::to_owned)
.ok_or_else(|| bad_args(format!("`{key}` must be an array of strings")))
})
.collect()
}
fn arg_object<'a>(args: &'a Json, key: &str) -> Result<Option<&'a Map<String, Json>>> {
match args.get(key) {
None | Some(Json::Null) => Ok(None),
Some(Json::Object(o)) => Ok(Some(o)),
Some(_) => Err(bad_args(format!("`{key}` must be an object"))),
}
}
fn hex_field(e: &Map<String, Json>, k: &str) -> Option<String> {
e.get(k).and_then(Json::as_str).map(str::to_owned)
}
fn arg_expect(args: &Json) -> Result<Option<Expect>> {
Ok(arg_object(args, "expect")?.map(|e| Expect {
content_hash: hex_field(e, "content_hash"),
parent_children_hash: hex_field(e, "parent_children_hash"),
}))
}
fn arg_parent_expect(args: &Json) -> Result<Option<Expect>> {
Ok(arg_object(args, "expect")?.map(|e| Expect {
content_hash: None,
parent_children_hash: hex_field(e, "parent_children_hash"),
}))
}
fn resolution_arg(args: &Json, default: Resolution) -> Result<Resolution> {
match arg_str(args, "resolution") {
None => Ok(default),
Some(s) => Resolution::parse(s).ok_or_else(|| {
bad_args("`resolution` must be one of skeleton, outline, text, raw, full")
}),
}
}
fn merge(mut a: Map<String, Json>, b: Json) -> Json {
if let Json::Object(o) = b {
for (k, v) in o {
a.insert(k, v);
}
}
Json::Object(a)
}
fn require_some(tool: &str, arg: &str, list: Vec<String>, spec: &ToolSpec) -> Result<Vec<String>> {
if list.is_empty() {
return Err(argument_error(
tool,
&format!("`{arg}` must name at least one block"),
&spec.input_schema,
));
}
Ok(list)
}
fn effect_expect(e: Option<Expect>) -> Option<EffectExpect> {
e.map(|e| EffectExpect {
content_hash: e.content_hash,
parent_children_hash: e.parent_children_hash,
revision: None,
})
}
fn placement_target(parent: Parent, doc_id: String) -> EffectTarget {
match parent {
Parent::Doc => EffectTarget::Document {
id: Some(doc_id),
path: None,
},
Parent::Block(id) => EffectTarget::Block { id },
Parent::Section { heading } => EffectTarget::Section {
heading_id: heading,
},
}
}
type SugarOutcome = (Plan, Option<Receipt>);
fn strip_effect_index(e: omgbase_store::Error) -> omgbase_store::Error {
match e {
omgbase_store::Error::Mutation(mut m) => {
m.data.remove("effect_index");
omgbase_store::Error::Mutation(m)
}
other => other,
}
}
fn sugar_json((plan, receipt): &SugarOutcome) -> Json {
match receipt {
Some(r) => receipt_json(r),
None => plan_json(plan),
}
}
fn schema(props: &[(&str, Json)], required: &[&str], repo: bool) -> Json {
let mut p = Map::new();
for (k, v) in props {
p.insert((*k).to_owned(), v.clone());
}
if repo {
p.insert("repo".to_owned(), json!({ "type": "string" }));
}
json!({ "type": "object", "properties": p, "required": required })
}
fn s() -> Json {
json!({ "type": "string" })
}
fn i() -> Json {
json!({ "type": "integer" })
}
fn b() -> Json {
json!({ "type": "boolean" })
}
fn strings() -> Json {
json!({ "type": "array", "items": { "type": "string" } })
}
fn obj() -> Json {
json!({ "type": "object" })
}
fn nullable_string() -> Json {
json!({ "type": ["string", "null"] })
}
fn resolution_schema() -> Json {
json!({ "type": "string", "enum": ["skeleton", "outline", "text", "raw", "full"] })
}
fn at_schema() -> Json {
json!({ "oneOf": [
{ "const": "start" }, { "const": "end" },
{ "type": "object", "properties": { "before": { "type": "string" } }, "required": ["before"] },
{ "type": "object", "properties": { "after": { "type": "string" } }, "required": ["after"] }
] })
}
fn expect_schema() -> Json {
json!({ "type": "object", "properties": { "content_hash": { "type": "string" }, "parent_children_hash": { "type": "string" } } })
}
fn effects_schema() -> Json {
json!({ "type": "array", "items": { "type": "object" } })
}
fn parent_expect_schema() -> Json {
json!({ "type": "object", "properties": { "parent_children_hash": { "type": "string" } } })
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Profile {
#[default]
Full,
Minimal,
}
pub const MINIMAL_TOOLS: &[&str] = &[
"query",
"query_syntax",
"docs_read",
"docs_tree",
"blocks_resolve",
"changes_since",
"plan",
"execute",
"apply",
"version",
];
impl Profile {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Full => "full",
Self::Minimal => "minimal",
}
}
#[must_use]
pub fn includes(self, tool: &str) -> bool {
match self {
Self::Full => true,
Self::Minimal => MINIMAL_TOOLS.contains(&tool),
}
}
#[must_use]
pub fn tools(self) -> Vec<ToolSpec> {
catalog()
.iter()
.filter(|t| self.includes(t.name))
.cloned()
.collect()
}
}
impl std::str::FromStr for Profile {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"full" => Ok(Self::Full),
"minimal" => Ok(Self::Minimal),
other => Err(format!(
"unknown tools profile {} (expected full|minimal)",
Json::String(other.to_owned())
)),
}
}
}
impl std::fmt::Display for Profile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[must_use]
pub fn tools() -> Vec<ToolSpec> {
catalog().to_vec()
}
pub(crate) fn catalog() -> &'static [ToolSpec] {
static CATALOG: std::sync::OnceLock<Vec<ToolSpec>> = std::sync::OnceLock::new();
CATALOG.get_or_init(build_catalog)
}
pub(crate) fn tool_spec(name: &str) -> Option<&'static ToolSpec> {
catalog().iter().find(|t| t.name == name)
}
#[allow(clippy::too_many_lines)]
fn build_catalog() -> Vec<ToolSpec> {
use crate::descriptions as d;
let doc = |extra: &[(&str, Json)], required: &[&str]| {
let mut props = vec![("id", s()), ("path", s())];
props.extend(extra.iter().cloned());
schema(&props, required, true)
};
vec![
ToolSpec {
name: "docs_outline",
description: d::DOCS_OUTLINE,
input_schema: doc(
&[
(
"resolution",
json!({ "type": "string", "enum": ["skeleton", "outline"] }),
),
("depth", i()),
("budget_tokens", i()),
],
&[],
),
},
ToolSpec {
name: "docs_read",
description: d::DOCS_READ,
input_schema: doc(&[("include_ids", b()), ("at", s())], &[]),
},
ToolSpec {
name: "docs_read_many",
description: d::DOCS_READ_MANY,
input_schema: schema(
&[
("docs", strings()),
("include_ids", b()),
("budget_tokens", i()),
],
&["docs"],
true,
),
},
ToolSpec {
name: "blocks_read",
description: d::BLOCKS_READ,
input_schema: schema(
&[("id", s()), ("resolution", resolution_schema())],
&["id"],
true,
),
},
ToolSpec {
name: "nodes_block_read",
description: d::NODES_BLOCK_READ,
input_schema: schema(
&[("id", s()), ("resolution", resolution_schema())],
&["id"],
true,
),
},
ToolSpec {
name: "blocks_read_many",
description: d::BLOCKS_READ_MANY,
input_schema: schema(
&[
("ids", strings()),
("resolution", resolution_schema()),
("budget_tokens", i()),
],
&["ids"],
true,
),
},
ToolSpec {
name: "docs_tree",
description: d::DOCS_TREE,
input_schema: schema(
&[
("path", s()),
("depth", i()),
("limit", i()),
("cursor", nullable_string()),
("budget_tokens", i()),
],
&[],
true,
),
},
ToolSpec {
name: "docs_list",
description: d::DOCS_LIST,
input_schema: schema(
&[
("path_glob", s()),
("limit", i()),
("cursor", nullable_string()),
("budget_tokens", i()),
],
&[],
true,
),
},
ToolSpec {
name: "query_syntax",
description: d::QUERY_SYNTAX,
input_schema: schema(&[], &[], false),
},
ToolSpec {
name: "query",
description: d::QUERY,
input_schema: schema(
&[
("query", s()),
("limit", i()),
("cursor", nullable_string()),
("bare", b()),
],
&["query"],
true,
),
},
ToolSpec {
name: "graph",
description: d::GRAPH,
input_schema: schema(
&[
("roots", strings()),
("degrees", i()),
(
"direction",
json!({ "type": "string", "enum": ["in", "out", "both"] }),
),
("predicate", s()),
("select", strings()),
("max_documents", i()),
],
&["roots"],
true,
),
},
ToolSpec {
name: "blocks_search",
description: d::BLOCKS_SEARCH,
input_schema: schema(&[("q", s()), ("limit", i())], &["q"], true),
},
ToolSpec {
name: "blocks_resolve",
description: d::BLOCKS_RESOLVE,
input_schema: schema(&[("query", s()), ("limit", i())], &["query"], true),
},
ToolSpec {
name: "apply",
description: d::APPLY,
input_schema: schema(
&[
("effects", effects_schema()),
("select", s()),
("expect", obj()),
("reason", s()),
],
&["effects"],
true,
),
},
ToolSpec {
name: "plan",
description: d::PLAN,
input_schema: schema(
&[
("effects", effects_schema()),
("select", s()),
("expect", obj()),
("diffs", b()),
],
&["effects"],
true,
),
},
ToolSpec {
name: "execute",
description: d::EXECUTE,
input_schema: schema(
&[
("plan", obj()),
("plan_id", s()),
("effects", effects_schema()),
("select", s()),
("expect", obj()),
("reason", s()),
],
&[],
true,
),
},
ToolSpec {
name: "blocks_insert",
description: d::BLOCKS_INSERT,
input_schema: schema(
&[
("to", s()),
("markdown", s()),
("at", at_schema()),
("expect", parent_expect_schema()),
],
&["to", "markdown"],
true,
),
},
ToolSpec {
name: "blocks_update",
description: d::BLOCKS_UPDATE,
input_schema: schema(
&[
("id", s()),
("markdown", s()),
("checked", b()),
("expect", expect_schema()),
],
&["id"],
true,
),
},
ToolSpec {
name: "blocks_move",
description: d::BLOCKS_MOVE,
input_schema: schema(
&[
("blocks", strings()),
("to", s()),
("at", at_schema()),
("expect", parent_expect_schema()),
],
&["blocks", "to"],
true,
),
},
ToolSpec {
name: "blocks_remove",
description: d::BLOCKS_REMOVE,
input_schema: schema(&[("blocks", strings())], &["blocks"], true),
},
ToolSpec {
name: "blocks_split",
description: d::BLOCKS_SPLIT,
input_schema: schema(
&[
("id", s()),
(
"at",
json!({ "type": "array", "items": { "type": "integer" } }),
),
],
&["id", "at"],
true,
),
},
ToolSpec {
name: "blocks_merge",
description: d::BLOCKS_MERGE,
input_schema: schema(
&[("blocks", strings()), ("separator", s())],
&["blocks"],
true,
),
},
ToolSpec {
name: "blocks_check",
description: d::BLOCKS_CHECK,
input_schema: schema(&[("ids", strings()), ("checked", b())], &["ids"], true),
},
ToolSpec {
name: "nodes_set",
description: d::NODES_SET,
input_schema: schema(
&[("id", s()), ("prop", s()), ("value", s())],
&["id", "prop", "value"],
true,
),
},
ToolSpec {
name: "blocks_append",
description: d::BLOCKS_APPEND,
input_schema: schema(&[("id", s()), ("text", s())], &["id", "text"], true),
},
ToolSpec {
name: "docs_append",
description: d::DOCS_APPEND,
input_schema: doc(&[("text", s()), ("section", s())], &["text"]),
},
ToolSpec {
name: "links_retarget",
description: d::LINKS_RETARGET,
input_schema: schema(
&[("from_target", s()), ("to_target", s()), ("path_glob", s())],
&["from_target", "to_target"],
true,
),
},
ToolSpec {
name: "links_stale",
description: d::LINKS_STALE,
input_schema: schema(
&[("path_glob", s()), ("limit", i()), ("summary", b())],
&[],
true,
),
},
ToolSpec {
name: "links_repair",
description: d::LINKS_REPAIR,
input_schema: schema(
&[
(
"repairs",
json!({ "type": "array", "items": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" } }, "required": ["from", "to"] } }),
),
("from_target", s()),
("to_target", s()),
("path_glob", s()),
],
&[],
true,
),
},
ToolSpec {
name: "docs_create",
description: d::DOCS_CREATE,
input_schema: schema(
&[("path", s()), ("markdown", s()), ("frontmatter", obj())],
&["path", "markdown"],
true,
),
},
ToolSpec {
name: "docs_move",
description: d::DOCS_MOVE,
input_schema: doc(&[("to_path", s()), ("retarget_inbound", b())], &["to_path"]),
},
ToolSpec {
name: "docs_delete",
description: d::DOCS_DELETE,
input_schema: doc(&[], &[]),
},
ToolSpec {
name: "docs_set_meta",
description: d::DOCS_SET_META,
input_schema: doc(&[("set", obj()), ("unset", strings())], &[]),
},
ToolSpec {
name: "docs_update",
description: d::DOCS_UPDATE,
input_schema: doc(&[("content", s()), ("reason", s())], &["content"]),
},
ToolSpec {
name: "observe",
description: d::OBSERVE,
input_schema: schema(
&[("path", s()), ("content", s())],
&["path", "content"],
true,
),
},
ToolSpec {
name: "observe_many",
description: d::OBSERVE_MANY,
input_schema: schema(
&[(
"files",
json!({ "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "content": { "type": "string" } }, "required": ["path", "content"] } }),
)],
&["files"],
true,
),
},
ToolSpec {
name: "observe_delete",
description: d::OBSERVE_DELETE,
input_schema: schema(&[("path", s())], &["path"], true),
},
ToolSpec {
name: "blocks_history",
description: d::BLOCKS_HISTORY,
input_schema: schema(&[("id", s()), ("limit", i())], &["id"], false),
},
ToolSpec {
name: "docs_diff",
description: d::DOCS_DIFF,
input_schema: doc(
&[("from_rev", s()), ("to_rev", s())],
&["from_rev", "to_rev"],
),
},
ToolSpec {
name: "docs_diff_unified",
description: d::DOCS_DIFF_UNIFIED,
input_schema: doc(&[("from_rev", s()), ("to_rev", s())], &[]),
},
ToolSpec {
name: "docs_read_at",
description: d::DOCS_READ_AT,
input_schema: doc(&[("rev", s())], &["rev"]),
},
ToolSpec {
name: "docs_history",
description: d::DOCS_HISTORY,
input_schema: schema(
&[
("path_glob", s()),
("id", s()),
("path", s()),
("include_deleted", b()),
("limit", i()),
],
&[],
true,
),
},
ToolSpec {
name: "changes_since",
description: d::CHANGES_SINCE,
input_schema: schema(
&[
("cursor", i()),
(
"origin",
json!({ "type": "string", "enum": ["api", "observed", "import"] }),
),
("limit", i()),
],
&[],
true,
),
},
ToolSpec {
name: "repos_status",
description: d::REPOS_STATUS,
input_schema: schema(&[], &[], true),
},
ToolSpec {
name: "sync_status",
description: d::SYNC_STATUS,
input_schema: schema(&[], &[], true),
},
ToolSpec {
name: "repos",
description: d::REPOS,
input_schema: schema(&[], &[], false),
},
ToolSpec {
name: "version",
description: d::VERSION,
input_schema: schema(&[], &[], false),
},
]
}
impl Surface {
#[must_use]
pub fn new(
store: Store,
default_repo: &str,
provider: Option<Box<dyn EmbeddingProvider>>,
) -> Self {
let _ = store
.conn()
.execute_batch("PRAGMA cache_size = -16000; PRAGMA temp_store = MEMORY;");
Self {
store,
default_repo: default_repo.to_owned(),
provider,
provider_name: None,
on_mutation: None,
clock: Box::new(omgbase_sync::now_ts),
writes: WriteTarget::Derived,
config: Config::default(),
actor: ACTOR.to_owned(),
build_info: BuildInfo::default(),
preview: false,
last_plan: None,
profile: Profile::Full,
vocabulary: VocabularyCache::default(),
}
}
#[must_use]
pub fn with_profile(mut self, profile: Profile) -> Self {
self.profile = profile;
self
}
#[must_use]
pub fn profile(&self) -> Profile {
self.profile
}
#[must_use]
pub fn with_provider_name(mut self, name: &str) -> Self {
self.provider_name = Some(name.to_owned());
self
}
#[must_use]
pub fn with_actor(mut self, actor: &str) -> Self {
self.set_actor(actor);
self
}
pub fn set_actor(&mut self, actor: &str) {
self.actor = actor.to_owned();
}
#[must_use]
pub fn with_doc_store(mut self, doc_store: Box<dyn DocStore>) -> Self {
self.writes = WriteTarget::Fixed(doc_store);
self
}
#[must_use]
pub fn with_clock(mut self, clock: impl FnMut() -> String + 'static) -> Self {
self.clock = Box::new(clock);
self
}
#[must_use]
pub fn with_mutation_hook(mut self, hook: impl FnMut() + 'static) -> Self {
self.on_mutation = Some(Box::new(hook));
self
}
#[must_use]
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
#[must_use]
pub fn with_build_info(mut self, build_info: BuildInfo) -> Self {
self.build_info = build_info;
self
}
#[must_use]
pub fn store(&self) -> &Store {
&self.store
}
pub fn store_mut(&mut self) -> &mut Store {
&mut self.store
}
#[must_use]
pub fn default_repo(&self) -> &str {
&self.default_repo
}
#[must_use]
pub fn tools(&self) -> Vec<ToolSpec> {
self.profile.tools()
}
#[must_use]
pub fn is_write_tool(name: &str) -> bool {
matches!(
name,
"apply"
| "plan"
| "execute"
| "blocks_insert"
| "blocks_update"
| "blocks_move"
| "blocks_remove"
| "blocks_split"
| "blocks_merge"
| "blocks_check"
| "nodes_set"
| "blocks_append"
| "docs_append"
| "links_retarget"
| "links_repair"
| "docs_create"
| "docs_move"
| "docs_delete"
| "docs_set_meta"
| "docs_update"
| "observe"
| "observe_many"
| "observe_delete"
)
}
pub fn call(&mut self, name: &str, args: Json) -> ToolOutcome {
match self.call_result(name, &args) {
Ok(body) => ToolOutcome {
body,
is_error: false,
},
Err(e) => ToolOutcome {
body: e.to_json(),
is_error: true,
},
}
}
fn now(&mut self) -> String {
(self.clock)()
}
fn notify(&mut self) {
if let Some(hook) = &mut self.on_mutation {
hook();
}
}
fn repo_rows(&self) -> Result<Vec<RepoRow>> {
Ok(omgbase_sync::workspace::list_repos(&self.store)?)
}
fn scope(&self, args: &Json) -> Result<(String, Option<String>)> {
let rows = self.repo_rows()?;
match arg_str(args, "repo") {
None => {
let root = rows
.iter()
.find(|r| r.repo_id == self.default_repo)
.and_then(|r| r.root_path.clone());
Ok((self.default_repo.clone(), root))
}
Some(slug) => rows
.iter()
.find(|r| r.slug == slug)
.map(|r| (r.repo_id.clone(), r.root_path.clone()))
.ok_or_else(|| {
SurfaceError::with_data(
"repo_not_found",
format!("no repo '{slug}' in this workspace"),
json!({ "repo": slug }),
)
}),
}
}
fn require_root(&self, root: Option<&str>) -> Result<()> {
if matches!(self.writes, WriteTarget::Fixed(_)) || root.is_some() {
Ok(())
} else {
Err(SurfaceError::new(
"repo_not_found",
"repo has no filesystem source; mutation disabled",
))
}
}
fn with_writes<T>(
&mut self,
root: Option<&str>,
f: impl FnOnce(&mut Store, &mut dyn DocStore) -> Result<T>,
) -> Result<T> {
self.require_root(root)?;
match &mut self.writes {
WriteTarget::Fixed(ds) => f(&mut self.store, ds.as_mut()),
WriteTarget::Derived => {
let mut fs = FsDocStore::new(root.expect("checked by require_root"));
f(&mut self.store, &mut fs)
}
}
}
pub fn locate_document(&self, repo_id: &str, args: &Json) -> Result<String> {
self.locate_doc(repo_id, "docs_update", args)
}
fn docs_read_at(&self, doc_id: &str, rev: &str) -> Result<Json> {
read::docs_read_at(&self.store, doc_id, rev)?.ok_or_else(|| {
SurfaceError::with_data(
"target_missing",
format!(
"no revision {} for document {doc_id}",
Json::String(rev.to_owned())
),
json!({ "doc": doc_id, "rev": rev }),
)
})
}
fn locate_doc(&self, repo_id: &str, tool: &str, args: &Json) -> Result<String> {
let (id_key, path_key) = ("id", "path");
let schema = &tool_spec(tool).expect("a catalog tool").input_schema;
let id = arg_str(args, id_key);
let path = arg_str(args, path_key);
if id.is_none() && path.is_none() {
return Err(argument_error(
tool,
&format!("pass {id_key} or {path_key}"),
schema,
));
}
let mut given = Map::new();
if let Some(v) = id {
given.insert(id_key.to_owned(), json!(v));
}
if let Some(v) = path {
given.insert(path_key.to_owned(), json!(v));
}
let missing = |value: &str| {
SurfaceError::with_data(
"doc_missing",
format!(
"no document with {id_key} or {path_key} {}",
Json::String(value.to_owned())
),
Json::Object(given.clone()),
)
};
let conn = self.store.conn();
let by_id = match id {
Some(v) => {
let info = read::find_doc_by_id(conn, v)?.filter(|i| i.repo_id == repo_id);
Some(info.ok_or_else(|| missing(v))?.doc_id)
}
None => None,
};
let by_path = match path {
Some(v) => Some(
read::find_doc_by_path(conn, repo_id, v)?
.ok_or_else(|| missing(v))?
.doc_id,
),
None => None,
};
if let (Some(a), Some(b)) = (&by_id, &by_path)
&& a != b
{
return Err(argument_error(
tool,
&format!(
"{id_key} {} and {path_key} {} name different documents",
Json::String(id.unwrap_or_default().to_owned()),
Json::String(path.unwrap_or_default().to_owned())
),
schema,
));
}
Ok(by_id.or(by_path).expect("one locator given"))
}
fn require_rankable(&self, repo_id: &str, semantic: bool) -> Result<()> {
let Some(provider) = self.provider.as_deref().filter(|_| semantic) else {
return Ok(());
};
let model = provider.model();
let any: Option<i64> = self
.store
.conn()
.query_row(
"SELECT 1 WHERE EXISTS (SELECT 1 FROM doc_embeddings de JOIN docs d ON d.doc_id = de.doc_id WHERE d.repo_id = ?1 AND de.model = ?2) OR EXISTS (SELECT 1 FROM embeddings WHERE model = ?2)",
params![repo_id, model],
|r| r.get(0),
)
.optional()?;
if any.is_some() {
return Ok(());
}
let who = match &self.provider_name {
Some(name) => format!("provider {} (model {model})", Json::String(name.clone())),
None => format!("model {model}"),
};
Err(SurfaceError::with_data(
"provider_unavailable",
format!(
"semantic(...) cannot rank: {who} is configured but nothing in this repository is embedded under it — run omg embed drain"
),
json!({ "provider": self.provider_name, "model": model }),
))
}
fn doc_of_block(&self, block_id: &str) -> Result<String> {
self.doc_id_of_block(block_id)?
.ok_or_else(|| block_missing(block_id))
}
fn resolve_section_heading(
&self,
doc_id: &str,
doc_path: &str,
section: &str,
call: (Option<&str>, Option<&str>, &str),
) -> Result<String> {
let conn = self.store.conn();
let needle = if section.trim_start_matches([' ', '\t']).starts_with('#') {
normalize_visible_text(section, BlockKind::Heading, 0)
} else {
normalize_text(section)
};
let mut stmt = conn.prepare(
"SELECT block_id, doc_id, text FROM blocks WHERE doc_id = ?1 AND type = 'heading' AND deleted_commit IS NULL",
)?;
let rows: Vec<(String, String, String)> = stmt
.query_map(params![doc_id], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
.collect::<std::result::Result<_, _>>()?;
let matches: Vec<&(String, String, String)> = rows
.iter()
.filter(|(_, _, text)| normalize_text(text) == needle)
.collect();
let shown = reference_path(doc_path);
let (id, path, text) = call;
match matches.len() {
0 => {
let near: Vec<&(String, String, String)> = rows
.iter()
.filter(|(_, _, t)| normalize_text(t).eq_ignore_ascii_case(&needle))
.collect();
let fix = match near.as_slice() {
[one] => fixes::append_into_heading(&one.0, text),
_ => fixes::append_without_section(id, path, text),
};
Err(SurfaceError::with_data(
"parent_missing",
format!("no heading {} in {shown}", Json::String(section.to_owned())),
json!({ "section": section, "path": shown, "fix": fix }),
))
}
1 => Ok(matches[0].0.clone()),
n => Err(SurfaceError::with_data(
"ambiguous_heading",
format!(
"heading {} matches {n} headings in {shown}; pass the heading's id to blocks_append",
Json::String(section.to_owned())
),
json!({
"section": section,
"path": shown,
"candidates": matches.iter().map(|(b, d, _)| json!({ "id": b, "doc_id": d })).collect::<Vec<_>>(),
"fix": fixes::append_into_heading(&matches[0].0, text),
}),
)),
}
}
fn block_of_node(&self, node_id: &str) -> Result<(String, String)> {
let node: Option<(String, Option<String>)> = self
.store
.conn()
.query_row(
"SELECT doc_id, block_id FROM nodes WHERE node_id = ?1",
params![node_id],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.optional()?;
let Some((doc_id, block_id)) = node else {
return Err(SurfaceError::with_data(
"block_missing",
format!("no node with id {}", Json::String(node_id.to_owned())),
json!({ "id": node_id }),
));
};
let live = match &block_id {
Some(b) => self.doc_id_of_block(b)?.is_some(),
None => false,
};
match block_id {
Some(b) if live => Ok((doc_id, b)),
_ => Err(node_has_no_block(node_id, &doc_id)),
}
}
fn resolve_block_ref(&self, repo_id: &str, r: &str) -> Result<String> {
match read::resolve_ref(self.store.conn(), repo_id, r)? {
Some(ResolvedRef::Block { block_id, .. }) => Ok(block_id),
_ => Err(block_missing(r)),
}
}
fn resolve_parent_ref(&self, repo_id: &str, r: &str) -> Result<(Parent, String)> {
match read::resolve_ref(self.store.conn(), repo_id, r)? {
Some(ResolvedRef::Block { doc_id, block_id }) => Ok((Parent::Block(block_id), doc_id)),
Some(ResolvedRef::Document { doc_id }) => Ok((Parent::Doc, doc_id)),
None => Err(SurfaceError::with_data(
"block_missing",
format!(
"no block or document with id or path {}",
Json::String(r.to_owned())
),
json!({ "ref": r }),
)),
}
}
fn doc_id_of_block(&self, block_id: &str) -> Result<Option<String>> {
Ok(self
.store
.conn()
.query_row(
"SELECT doc_id FROM blocks WHERE block_id = ?1 AND deleted_commit IS NULL",
params![block_id],
|r| r.get(0),
)
.optional()?)
}
fn resolve_at(&self, repo_id: &str, at: Option<&Json>) -> Result<At> {
match at {
None | Some(Json::Null) => Ok(At::End),
Some(Json::String(s)) if s == "start" => Ok(At::Start),
Some(Json::String(s)) if s == "end" => Ok(At::End),
Some(Json::Object(o)) => {
if let Some(b) = o.get("before").and_then(Json::as_str) {
return Ok(At::Before(self.resolve_block_ref(repo_id, b)?));
}
if let Some(a) = o.get("after").and_then(Json::as_str) {
return Ok(At::After(self.resolve_block_ref(repo_id, a)?));
}
Err(bad_args(
"`at` must be \"start\", \"end\", {before} or {after}",
))
}
Some(_) => Err(bad_args(
"`at` must be \"start\", \"end\", {before} or {after}",
)),
}
}
fn apply_sugar(
&mut self,
repo_id: &str,
root: Option<&str>,
effects: Vec<PublicEffect>,
reason: &str,
) -> Result<Json> {
let out = self.apply_sugar_with(repo_id, root, reason, |_, _| Ok(effects))?;
Ok(sugar_json(&out))
}
fn apply_sugar_with(
&mut self,
repo_id: &str,
root: Option<&str>,
reason: &str,
build: impl FnOnce(&mut Store, &mut dyn DocStore) -> Result<Vec<PublicEffect>>,
) -> Result<SugarOutcome> {
let origin = ApplyOrigin::new(&self.actor, Some(reason));
let ctx = self.plan_ctx(repo_id);
let preview = self.preview;
let out = self.with_writes(root, |store, ds| {
let effects = build(store, ds)?;
if preview {
let plan = store
.plan_effects(&ctx, &effects, ds)
.map_err(strip_effect_index)?;
return Ok((plan, None));
}
let (plan, receipt) = store
.apply_effects_detailed(&ctx, &effects, &origin, ds)
.map_err(strip_effect_index)?;
Ok((plan, Some(receipt)))
})?;
self.last_plan = Some(out.0.clone());
if out.1.is_some() {
self.notify();
}
Ok(out)
}
fn doc_info(&self, repo_id: &str, doc_ref: &str) -> Result<omgbase_store::DocInfo> {
self.store
.find_doc_by_ref(repo_id, doc_ref)?
.ok_or_else(|| {
SurfaceError::from(MutationError::new(
ErrorCode::DocMissing,
format!("no document {doc_ref}"),
))
})
}
pub fn call_result_with_plan(
&mut self,
name: &str,
args: &Json,
) -> Result<(Json, Option<Json>)> {
self.last_plan = None;
let result = self.call_result(name, args);
let plan = self.last_plan.take().map(|p| plan_json(&p));
Ok((result?, plan))
}
pub fn preview_call(&mut self, name: &str, args: &Json) -> Result<Json> {
self.preview = true;
let out = self.call_result(name, args);
self.preview = false;
out
}
fn apply_effects_detailed(
&mut self,
repo: &str,
root: Option<&str>,
args: &Json,
) -> Result<(Json, Option<Json>)> {
let reason = arg_str(args, "reason").map(str::to_owned);
let origin = ApplyOrigin::new(&self.actor, reason.as_deref());
let ctx = self.plan_ctx(repo);
let preview = self.preview;
let (plan, receipt) = match SelectRequest::from_args(args)? {
Some(req) => {
let semantic = self.selector_semantic(repo, req.select_str())?;
self.with_writes(root, |store, ds| {
if preview {
return Ok((
select::plan_selected(store, ds, &ctx, &req, semantic)?,
None,
));
}
if req.has_selector() && req.expect.is_none() {
let x = select::expand(store, &ctx, ds, &req, semantic)?;
let cardinality = x.selection.map_or(0, |s| s.cardinality);
return Err(select::unbounded_apply(&req, cardinality));
}
let (plan, receipt) = store.apply_planned(&ctx, &origin, ds, |store, ds| {
select::plan_selected(store, ds, &ctx, &req, semantic)
})?;
Ok((plan, Some(receipt)))
})?
}
None => {
self.with_writes(root, |store, ds| {
let effects = select::validate_plain(store, &args["effects"])?;
if preview {
return Ok((store.plan_effects(&ctx, &effects, ds)?, None));
}
let (plan, receipt) =
store.apply_effects_detailed(&ctx, &effects, &origin, ds)?;
Ok((plan, Some(receipt)))
})?
}
};
self.last_plan = Some(plan.clone());
if receipt.is_some() {
self.notify();
}
Ok((plan_json(&plan), receipt.as_ref().map(receipt_json)))
}
fn selector_semantic(
&self,
repo_id: &str,
select: &str,
) -> Result<HashMap<String, crate::context::SemanticVec>> {
let phrases = crate::query::collect_semantic_phrases(select);
if self.provider.is_none() && !phrases.is_empty() {
return Err(provider_unavailable());
}
self.require_rankable(repo_id, !phrases.is_empty())?;
embed_phrases(self.provider.as_deref(), phrases)
}
fn plan_ctx(&mut self, repo_id: &str) -> PlanContext {
PlanContext {
repo_id: repo_id.to_owned(),
ts: self.now(),
config: self.config.clone(),
}
}
pub fn call_result(&mut self, name: &str, args: &Json) -> Result<Json> {
self.call_result_inner(name, args)
.map_err(|e| fixes::attach(name, args, e, |i| self.allocated_before(args, i)))
}
fn allocated_before(&mut self, args: &Json, i: usize) -> Option<usize> {
if i == 0 {
return Some(0);
}
let prefix = args.get("effects")?.as_array()?.get(..i)?.to_vec();
let effects = parse_effects(&Json::Array(prefix)).ok()?;
let (repo, root) = self.scope(args).ok()?;
let ctx = self.plan_ctx(&repo);
self.with_writes(root.as_deref(), |store, ds| {
Ok(store.plan_effects(&ctx, &effects, ds)?)
})
.ok()
.map(|p| p.new_ids.len())
}
#[allow(clippy::too_many_lines)]
fn call_result_inner(&mut self, name: &str, args: &Json) -> Result<Json> {
let spec =
tool_spec(name).ok_or_else(|| SurfaceError::other(format!("unknown tool {name}")))?;
let normalized = validate_arguments(name, &spec.input_schema, args)?;
let args = normalized.as_ref().unwrap_or(args);
match name {
"docs_outline" => {
let (repo, _) = self.scope(args)?;
let doc_id = self.locate_doc(&repo, name, args)?;
let skeleton = match arg_str(args, "resolution") {
None | Some("outline") => false,
Some("skeleton") => true,
Some(_) => return Err(bad_args("`resolution` must be skeleton or outline")),
};
read::docs_outline(
&self.store,
&doc_id,
skeleton,
arg_i64(args, "depth")?,
arg_usize(args, "budget_tokens")?,
)
}
"docs_read" => {
let (repo, _) = self.scope(args)?;
let doc_id = self.locate_doc(&repo, name, args)?;
if let Some(rev) = arg_str(args, "at") {
if arg_bool(args, "include_ids")?.unwrap_or(false) {
return Err(argument_error(
name,
"`include_ids` is not available with `at`",
&tool_spec(name).expect("a catalog tool").input_schema,
));
}
return self.docs_read_at(&doc_id, rev);
}
read::docs_read(
&self.store,
&doc_id,
arg_bool(args, "include_ids")?.unwrap_or(false),
)?
.ok_or_else(|| {
SurfaceError::with_data(
"doc_missing",
format!(
"no document with id or path {}",
Json::String(
arg_str(args, "id")
.or(arg_str(args, "path"))
.unwrap_or_default()
.to_owned()
)
),
json!({ "id": doc_id }),
)
})
}
"docs_read_many" => {
let (repo, _) = self.scope(args)?;
let refs = arg_strings(args, "docs")?;
read::docs_read_many(
&self.store,
&repo,
&refs,
arg_bool(args, "include_ids")?.unwrap_or(false),
arg_usize(args, "budget_tokens")?,
)
}
"blocks_read" => {
self.scope(args)?;
let id = arg_string(args, "id")?;
if id.starts_with("n_") {
return Err(SurfaceError::with_data(
"block_missing",
format!(
"no block with id {} (an n_ id names a node — nodes_block_read reads its block)",
Json::String(id.clone())
),
json!({ "id": id }),
));
}
let doc_id = self.doc_of_block(&id)?;
read::blocks_read(
&self.store,
&doc_id,
&id,
resolution_arg(args, Resolution::Full)?,
)?
.ok_or_else(|| block_missing(&id))
}
"nodes_block_read" => {
self.scope(args)?;
let id = arg_string(args, "id")?;
let (doc_id, block_id) = self.block_of_node(&id)?;
read::blocks_read(
&self.store,
&doc_id,
&block_id,
resolution_arg(args, Resolution::Full)?,
)?
.ok_or_else(|| node_has_no_block(&id, &doc_id))
}
"blocks_read_many" => {
self.scope(args)?;
let ids = arg_strings(args, "ids")?;
read::blocks_read_many(
&self.store,
None,
&ids,
resolution_arg(args, Resolution::Text)?,
arg_usize(args, "budget_tokens")?,
)
}
"docs_tree" => {
let (repo, _) = self.scope(args)?;
read::docs_tree(
&self.store,
&repo,
arg_str(args, "path"),
arg_i64(args, "depth")?,
arg_i64(args, "limit")?,
arg_str(args, "cursor"),
arg_usize(args, "budget_tokens")?,
)
}
"docs_list" => {
let (repo, _) = self.scope(args)?;
read::docs_list(
&self.store,
&repo,
arg_str(args, "path_glob"),
arg_i64(args, "limit")?,
arg_str(args, "cursor"),
arg_usize(args, "budget_tokens")?,
)
}
"query_syntax" => Ok(json!({ "syntax": QUERY_SYNTAX })),
"query" => {
let (repo, _) = self.scope(args)?;
let source = arg_string(args, "query")?;
if self.provider.is_none()
&& !crate::query::collect_semantic_phrases(&source).is_empty()
{
return Err(provider_unavailable());
}
let now_ms = dates::stamp_ms(&self.now());
let opts = QueryOptions {
limit: arg_usize(args, "limit")?,
cursor: arg_str(args, "cursor"),
provider: self.provider.as_deref(),
in_memory: false,
bare: arg_bool(args, "bare")?.unwrap_or(false),
unknown_names: None,
vocabulary: Some(&self.vocabulary),
now_ms: Some(now_ms),
};
self.require_rankable(
&repo,
!crate::query::collect_semantic_phrases(&source).is_empty(),
)?;
Ok(query(&self.store, &repo, &source, opts)?.to_json())
}
"graph" => {
let (repo, _) = self.scope(args)?;
let g = GraphArgs {
roots: arg_strings(args, "roots")?,
degrees: arg_i64(args, "degrees")?,
direction: arg_str(args, "direction").map(str::to_owned),
predicate: arg_str(args, "predicate").map(str::to_owned),
select: if args.get("select").is_some() {
arg_strings(args, "select")?
} else {
Vec::new()
},
max_documents: arg_i64(args, "max_documents")?,
};
if self.provider.is_none()
&& g.select.iter().any(|s| {
!crate::query::collect_semantic_phrases(&format!("from docs select x: {s}"))
.is_empty()
})
{
return Err(provider_unavailable());
}
self.require_rankable(
&repo,
g.select.iter().any(|s| {
!crate::query::collect_semantic_phrases(&format!("from docs select x: {s}"))
.is_empty()
}),
)?;
let now_ms = dates::stamp_ms(&self.now());
graph_neighborhood(
&self.store,
&repo,
&g,
self.provider.as_deref(),
Some(now_ms),
)
}
"blocks_search" => {
let (repo, _) = self.scope(args)?;
let q = arg_string(args, "q")?;
let res =
self.store
.text_search(&repo, &q, arg_usize(args, "limit")?.unwrap_or(50))?;
Ok(json!({
"hits": res.hits.iter().map(|h| json!({
"id": h.block_id, "doc_id": h.doc_id, "path": reference_path(&h.path), "type": h.block_type, "text": h.text, "score": h.score,
})).collect::<Vec<_>>(),
"truncated": res.truncated,
}))
}
"blocks_resolve" => {
let (repo, _) = self.scope(args)?;
let q = arg_string(args, "query")?;
let vector = match &self.provider {
Some(p) => Some(QueryVector {
model: p.model().to_owned(),
vec: p
.embed_query(&q)
.map_err(|e| SurfaceError::new(e.code(), e.to_string()))?,
}),
None => None,
};
let hits = self
.store
.resolve(&repo, &q, vector, arg_usize(args, "limit")?)?;
let locator = |l: &str| match l.find('#') {
Some(i) => format!("{}{}", reference_path(&l[..i]), &l[i..]),
None => l.to_owned(),
};
Ok(Json::Array(hits.iter().map(|h| json!({
"id": h.id, "locator": locator(&h.locator), "preview": h.preview, "evidence": evidence_json(&h.evidence),
})).collect()))
}
"apply" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let (plan, receipt) = self.apply_effects_detailed(&repo, root.as_deref(), args)?;
Ok(receipt.unwrap_or(plan))
}
"plan" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let ctx = self.plan_ctx(&repo);
let plan = match SelectRequest::from_args(args)? {
Some(req) => {
let semantic = self.selector_semantic(&repo, req.select_str())?;
self.with_writes(root.as_deref(), |store, ds| {
select::plan_selected(store, ds, &ctx, &req, semantic)
})?
}
None => self.with_writes(root.as_deref(), |store, ds| {
let effects = select::validate_plain(store, &args["effects"])?;
Ok(store.plan_effects(&ctx, &effects, ds)?)
})?,
};
let mut plan = plan_json(&plan);
if args.get("diffs").and_then(Json::as_bool) != Some(true)
&& let Some(preview) = plan.get_mut("preview").and_then(Json::as_object_mut)
{
preview.remove("diffs");
}
Ok(plan)
}
"execute" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let reason = arg_str(args, "reason").map(str::to_owned);
let origin = ApplyOrigin::new(&self.actor, reason.as_deref());
let ctx = self.plan_ctx(&repo);
let plan_form = args.get("plan").is_some();
let digest_form = args.get("plan_id").is_some()
|| args.get("effects").is_some()
|| args.get("select").is_some()
|| args.get("expect").is_some();
if plan_form == digest_form {
return Err(argument_error(
name,
"pass exactly one of `plan` or `plan_id` + `effects`",
&spec.input_schema,
));
}
let receipt = if plan_form {
let plan = match Plan::admit(&args["plan"]) {
Ok(plan) => plan,
Err(Admission::Refused(m)) => return Err(SurfaceError::from(m)),
Err(Admission::Invalid(m)) => {
return Err(SurfaceError::filter_invalid(m, "arguments"));
}
};
self.with_writes(root.as_deref(), |store, ds| {
Ok(store.execute_plan(&ctx, &plan, &origin, ds)?)
})?
} else {
let (Some(plan_id), Some(effects)) =
(arg_str(args, "plan_id"), args.get("effects"))
else {
return Err(argument_error(
name,
"the digest form needs both `plan_id` and `effects`",
&spec.input_schema,
));
};
let plan_id = plan_id.to_owned();
match SelectRequest::from_args(args)? {
Some(req) => {
let semantic = self.selector_semantic(&repo, req.select_str())?;
self.with_writes(root.as_deref(), |store, ds| {
store.execute_replanned(&ctx, &plan_id, &origin, ds, |store, ds| {
select::plan_selected(store, ds, &ctx, &req, semantic)
})
})?
}
None => self.with_writes(root.as_deref(), |store, ds| {
let effects = select::validate_plain(store, effects)?;
Ok(store.execute_by_digest(&ctx, &plan_id, &effects, &origin, ds)?)
})?,
}
};
self.notify();
Ok(receipt_json(&receipt))
}
"blocks_insert" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let (parent, doc_id) = self.resolve_parent_ref(&repo, &arg_string(args, "to")?)?;
let at = self.resolve_at(&repo, args.get("at"))?;
let effects = sugar::insert(
placement_target(parent, doc_id),
at,
&arg_string(args, "markdown")?,
effect_expect(arg_parent_expect(args)?),
);
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_insert")
}
"blocks_update" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let block = self.resolve_block_ref(&repo, &arg_string(args, "id")?)?;
let markdown = arg_str(args, "markdown");
let checked = arg_bool(args, "checked")?;
let effects = sugar::update_block(
&block,
markdown,
checked,
effect_expect(arg_expect(args)?),
);
if effects.is_empty() {
return Err(argument_error(
name,
"pass `markdown` and/or `checked`",
&spec.input_schema,
));
}
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_update")
}
"blocks_move" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let blocks: Vec<String> =
require_some(name, "blocks", arg_strings(args, "blocks")?, spec)?
.iter()
.map(|b| self.resolve_block_ref(&repo, b))
.collect::<Result<_>>()?;
let to_ref = arg_string(args, "to")?;
let (parent, doc_id) = self.resolve_parent_ref(&repo, &to_ref)?;
if parent == Parent::Doc {
if let Some(first) = blocks.first() {
if self.doc_id_of_block(first)? != Some(doc_id.clone()) {
return Err(SurfaceError::with_data(
"target_missing",
format!(
"blocks_move cannot target another document's root ({to_ref}); anchor on a block in that document with at.before/at.after"
),
json!({ "to": to_ref }),
));
}
}
}
let at = self.resolve_at(&repo, args.get("at"))?;
let effects = sugar::move_blocks(
blocks,
placement_target(parent, doc_id),
at,
effect_expect(arg_parent_expect(args)?),
);
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_move")
}
"blocks_remove" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let blocks: Vec<String> =
require_some(name, "blocks", arg_strings(args, "blocks")?, spec)?
.iter()
.map(|b| self.resolve_block_ref(&repo, b))
.collect::<Result<_>>()?;
let effects = sugar::remove_blocks(blocks);
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_remove")
}
"blocks_split" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let block = self.resolve_block_ref(&repo, &arg_string(args, "id")?)?;
let at: Vec<usize> = args
.get("at")
.and_then(Json::as_array)
.ok_or_else(|| bad_args("`at` must be an array of byte offsets"))?
.iter()
.map(|v| {
v.as_u64()
.map(|n| usize::try_from(n).unwrap_or(usize::MAX))
.ok_or_else(|| bad_args("`at` must be an array of byte offsets"))
})
.collect::<Result<_>>()?;
let effects = sugar::split_block(&block, at);
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_split")
}
"blocks_merge" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let blocks: Vec<String> =
require_some(name, "blocks", arg_strings(args, "blocks")?, spec)?
.iter()
.map(|b| self.resolve_block_ref(&repo, b))
.collect::<Result<_>>()?;
let effects = sugar::merge_blocks(blocks, arg_str(args, "separator"));
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_merge")
}
"blocks_check" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let blocks: Vec<String> =
require_some(name, "ids", arg_strings(args, "ids")?, spec)?
.iter()
.map(|b| self.resolve_block_ref(&repo, b))
.collect::<Result<_>>()?;
let effects =
sugar::check_blocks(&blocks, arg_bool(args, "checked")?.unwrap_or(true));
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_check")
}
"nodes_set" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let node_id = arg_string(args, "id")?;
let known: Option<i64> = self
.store
.conn()
.query_row(
"SELECT 1 FROM nodes WHERE node_id = ?1 AND repo_id = ?2",
params![node_id, repo],
|r| r.get(0),
)
.optional()?;
if known.is_none() {
return Err(SurfaceError::with_data(
"block_missing",
format!("no node with id {}", Json::String(node_id.clone())),
json!({ "id": node_id }),
));
}
let effects = sugar::set_node(
&node_id,
&arg_string(args, "prop")?,
&arg_string(args, "value")?,
);
self.apply_sugar(&repo, root.as_deref(), effects, "nodes_set")
}
"blocks_append" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let given = arg_string(args, "id")?;
let block = self.resolve_block_ref(&repo, &given)?;
let text = arg_string(args, "text")?;
type AppendRow = (String, Option<String>, i64, Option<String>, String);
let row: Option<AppendRow> = self
.store
.conn()
.query_row(
"SELECT b.type, d.format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.deleted_commit IS NULL), b.parent_block, d.path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?1 AND b.deleted_commit IS NULL",
params![block],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)),
)
.optional()?;
let Some((kind, format, children, parent, doc_path)) = row else {
return Err(block_missing(&given));
};
let container = if format.as_deref().unwrap_or("markdown") == "markdown" {
kind.parse::<BlockKind>().is_ok_and(|k| k.is_container())
} else {
children > 0
};
let into = if kind == "heading" {
EffectTarget::Section { heading_id: block }
} else if container {
EffectTarget::Block { id: block }
} else {
return Err(SurfaceError::with_data(
"block_not_container",
format!(
"block {block} is a {kind}, not a heading or a container; append means into — insert after it with blocks_insert {{ to: <its parent>, at: {{ after: {} }} }}",
Json::String(block.clone())
),
json!({
"id": block,
"type": kind,
"fix": fixes::append_after(
parent.as_deref().unwrap_or(&reference_path(&doc_path)),
&block,
&text,
),
}),
));
};
let effects = sugar::insert(into, At::End, &text, None);
self.apply_sugar(&repo, root.as_deref(), effects, "blocks_append")
}
"docs_append" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let doc_id = self.locate_doc(&repo, name, args)?;
let text = arg_string(args, "text")?;
let into = match arg_str(args, "section") {
Some(section) => {
let info = read::find_doc_by_id(self.store.conn(), &doc_id)?
.ok_or_else(|| block_missing(&doc_id))?;
let heading = self.resolve_section_heading(
&doc_id,
&info.path,
section,
(arg_str(args, "id"), arg_str(args, "path"), &text),
)?;
EffectTarget::Section {
heading_id: heading,
}
}
None => EffectTarget::Document {
id: Some(doc_id),
path: None,
},
};
let effects = sugar::insert(into, At::End, &text, None);
self.apply_sugar(&repo, root.as_deref(), effects, "docs_append")
}
"links_retarget" | "links_repair" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let repairs: Vec<omgbase_store::LinkRepair> = if name == "links_retarget" {
vec![omgbase_store::LinkRepair {
from: arg_string(args, "from_target")?,
to: arg_string(args, "to_target")?,
}]
} else if let Some(list) = args.get("repairs").and_then(Json::as_array) {
list.iter()
.map(|r| {
Ok(omgbase_store::LinkRepair {
from: arg_string(r, "from")?,
to: arg_string(r, "to")?,
})
})
.collect::<Result<_>>()?
} else if let (Some(f), Some(t)) =
(arg_str(args, "from_target"), arg_str(args, "to_target"))
{
vec![omgbase_store::LinkRepair {
from: f.to_owned(),
to: t.to_owned(),
}]
} else {
Vec::new()
};
if repairs.is_empty() {
return Err(SurfaceError::new(
"target_missing",
"links_repair requires `repairs` (array of {from,to}) or a `from_target`+`to_target` pair",
));
}
let path_glob = arg_str(args, "path_glob").filter(|g| !g.is_empty());
let plan = self.store.links_repair(&repo, &repairs, path_glob)?;
let pairs: Vec<(String, String)> = repairs
.iter()
.map(|r| (r.from.clone(), r.to.clone()))
.collect();
let effects = sugar::retarget(&pairs, path_glob);
let out =
self.apply_sugar_with(&repo, root.as_deref(), name, |_, _| Ok(effects))?;
let mut m = Map::new();
m.insert(
"hits".to_owned(),
Json::Array(plan.hits.iter().map(|h| json!({ "id": h.block, "path": reference_path(&h.path), "oldRaw": h.old_raw, "newRaw": h.new_raw })).collect()),
);
m.insert(
"pairs".to_owned(),
Json::Array(
plan.pairs
.iter()
.map(|p| json!({ "from": p.from, "to": p.to, "hits": p.hits }))
.collect(),
),
);
Ok(merge(m, sugar_json(&out)))
}
"links_stale" => {
let (repo, _) = self.scope(args)?;
let glob = arg_str(args, "path_glob").filter(|g| !g.is_empty());
if arg_bool(args, "summary")?.unwrap_or(false) {
return links::links_stale_summary(&self.store, &repo, glob);
}
links::links_stale(&self.store, &repo, glob, arg_i64(args, "limit")?)
}
"docs_create" => {
let (repo, root) = self.scope(args)?;
let fm = arg_object(args, "frontmatter")?.cloned();
let sugar::Sugar { effects, reason } = sugar::create_document(
&arg_string(args, "path")?,
&arg_string(args, "markdown")?,
fm.as_ref(),
);
let rel = canonical(&arg_string(args, "path")?);
let out = self.apply_sugar_with(&repo, root.as_deref(), &reason, |store, ds| {
if store.live_doc_at(&repo, &rel)? {
return Err(path_taken(format!("document already exists at {rel}")));
}
if ds.exists(&rel) {
return Err(path_taken(format!("file already exists on disk at {rel}")));
}
Ok(effects)
})?;
Ok(sugar_json(&out))
}
"docs_move" => {
let (repo, root) = self.scope(args)?;
let doc = self.locate_doc(&repo, name, args)?;
let to_rel = canonical(&arg_string(args, "to_path")?);
let retarget = arg_bool(args, "retarget_inbound")?.unwrap_or(true);
let info = self.doc_info(&repo, &doc)?;
let sugar::Sugar { effects, reason } =
sugar::move_document(&info.doc_id, &info.path, &to_rel, retarget);
let mut report: Option<(Vec<omgbase_store::InboundLink>, Option<Retargeted>)> =
None;
let out = self.apply_sugar_with(&repo, root.as_deref(), &reason, |store, ds| {
if store.live_doc_at(&repo, &to_rel)? {
return Err(path_taken(format!("a document already exists at {to_rel}")));
}
let inbound = inbound_links_to(store.conn(), &repo, &info.doc_id, &info.path)?;
if ds.exists(&to_rel) {
return Err(path_taken(format!(
"file already exists on disk at {to_rel}"
)));
}
report = Some(if retarget && !inbound.is_empty() {
let p = store.plan_inbound_retarget(
&info.doc_id,
&info.path,
&to_rel,
&inbound,
)?;
(
p.dangling,
Some(Retargeted {
blocks: p.blocks,
docs: p.docs,
}),
)
} else {
(inbound, None)
});
Ok(effects)
})?;
let (dangling, retargeted) = report.expect("set before the effects were built");
if out.1.is_none() {
return Ok(sugar_json(&out));
}
let mut m = Map::new();
m.insert(
"retargeted".to_owned(),
retargeted_json(retargeted.as_ref()),
);
m.insert(
"dangling".to_owned(),
Json::Array(dangling.iter().map(dangling_json).collect()),
);
let Json::Object(receipt) = sugar_json(&out) else {
unreachable!("a receipt is an object")
};
Ok(merge(receipt, Json::Object(m)))
}
"docs_delete" => {
let (repo, root) = self.scope(args)?;
let doc = self.locate_doc(&repo, name, args)?;
let info = self.doc_info(&repo, &doc)?;
let sugar::Sugar { effects, reason } =
sugar::delete_document(&info.doc_id, &info.path);
self.apply_sugar(&repo, root.as_deref(), effects, &reason)
}
"docs_set_meta" => {
let (repo, root) = self.scope(args)?;
let doc = self.locate_doc(&repo, name, args)?;
let set = arg_object(args, "set")?.cloned();
let unset = if args.get("unset").is_some() {
arg_strings(args, "unset")?
} else {
Vec::new()
};
let info = self.doc_info(&repo, &doc)?;
let sugar::Sugar { effects, reason } =
sugar::set_meta(&info.doc_id, &info.path, set.as_ref(), &unset);
if effects.is_empty() {
return Err(argument_error(
name,
"pass `set` and/or `unset`",
&spec.input_schema,
));
}
self.apply_sugar(&repo, root.as_deref(), effects, &reason)
}
"docs_update" => {
let (repo, root) = self.scope(args)?;
self.require_root(root.as_deref())?;
let doc = self.locate_doc(&repo, name, args)?;
let info = self.doc_info(&repo, &doc)?;
let content = arg_string(args, "content")?;
let reason = arg_str(args, "reason").map(str::to_owned);
let origin = ApplyOrigin::new(&self.actor, reason.as_deref());
let ctx = self.plan_ctx(&repo);
let effects = sugar::update_content(&info.doc_id, &content);
let preview = self.preview;
let (plan, receipt) = self.with_writes(root.as_deref(), |store, ds| {
if preview {
return Ok((store.plan_effects(&ctx, &effects, ds)?, None));
}
let (plan, receipt) = store
.apply_effects_detailed(&ctx, &effects, &origin, ds)
.map_err(strip_effect_index)?;
Ok((plan, Some(receipt)))
})?;
self.last_plan = Some(plan.clone());
if receipt.as_ref().is_some_and(|r| r.commit.is_some()) {
self.notify();
}
Ok(sugar_json(&(plan, receipt)))
}
"observe" => {
let (repo, _) = self.scope(args)?;
let path = storage_path(&arg_string(args, "path")?).to_owned();
let content = arg_string(args, "content")?;
let ts = self.now();
let config = self.config.clone();
let out = self
.store
.observe_one(&repo, &path, &content, &ts, &config)?;
self.store.sweep_pool(&ts)?;
self.notify();
Ok(observe_json(&out))
}
"observe_many" => {
let (repo, _) = self.scope(args)?;
let files = args
.get("files")
.and_then(Json::as_array)
.ok_or_else(|| bad_args("`files` must be an array of {path, content}"))?;
let items: Vec<omgbase_store::BatchItem> = files
.iter()
.map(|f| {
Ok(omgbase_store::BatchItem::observed(
storage_path(&arg_string(f, "path")?),
&arg_string(f, "content")?,
))
})
.collect::<Result<_>>()?;
let ts = self.now();
let config = self.config.clone();
let outcomes = self.store.observe_batch(&repo, &items, &ts, &config)?;
self.store.sweep_pool(&ts)?;
self.notify();
let mut out = Vec::with_capacity(outcomes.len());
for o in &outcomes {
match o.as_observed() {
Some(obs) => out.push(observe_json(obs)),
None => {
return Err(SurfaceError::other(format!(
"observe_many: unexpected outcome for {}",
o.path()
)));
}
}
}
Ok(Json::Array(out))
}
"observe_delete" => {
let (repo, _) = self.scope(args)?;
let path = storage_path(&arg_string(args, "path")?).to_owned();
let ts = self.now();
let out = self.store.observe_delete(&repo, &path, &ts)?;
self.notify();
Ok(
json!({ "id": out.doc_id, "path": reference_path(&out.path), "deleted": out.deleted() }),
)
}
"blocks_history" => history::blocks_history(
&self.store,
&arg_string(args, "id")?,
arg_i64(args, "limit")?,
),
"docs_diff" => {
let (repo, _) = self.scope(args)?;
let doc_id = self.locate_doc(&repo, name, args)?;
history::diff_blocks(
&self.store,
&doc_id,
&arg_string(args, "from_rev")?,
&arg_string(args, "to_rev")?,
)
}
"docs_diff_unified" => {
let (repo, _) = self.scope(args)?;
let doc_ref = arg_str(args, "id")
.or(arg_str(args, "path"))
.unwrap_or_default()
.to_owned();
let doc_id = self.locate_doc(&repo, name, args)?;
let revs = history::recent_revs(self.store.conn(), &doc_id)?;
let to_rev = arg_str(args, "to_rev")
.map(str::to_owned)
.or_else(|| revs.first().cloned());
let from_rev = arg_str(args, "from_rev")
.map(str::to_owned)
.or_else(|| revs.get(1).cloned())
.or_else(|| revs.first().cloned());
let (Some(from), Some(to)) = (from_rev, to_rev) else {
return Err(SurfaceError::new(
"target_missing",
format!("no revisions to diff for {}", Json::String(doc_ref)),
));
};
let path: Option<String> = self
.store
.conn()
.query_row(
"SELECT path FROM docs WHERE doc_id = ?1",
params![doc_id],
|r| r.get(0),
)
.optional()?;
let diff = history::diff_unified_text(&self.store, &doc_id, &from, &to)?;
Ok(
json!({ "id": doc_id, "path": path.as_deref().map_or_else(String::new, reference_path), "from": from, "to": to, "diff": diff }),
)
}
"docs_read_at" => {
let (repo, _) = self.scope(args)?;
let doc_id = self.locate_doc(&repo, name, args)?;
let rev = arg_string(args, "rev")?;
self.docs_read_at(&doc_id, &rev)
}
"docs_history" => {
let (repo, _) = self.scope(args)?;
let glob = arg_str(args, "path_glob").filter(|g| !g.is_empty());
let id = arg_str(args, "id");
let path = arg_str(args, "path");
if glob.is_none() && id.is_none() && path.is_none() {
return Err(SurfaceError::new(
"target_missing",
"docs_history requires one of path_glob, id or path",
));
}
let include_deleted = arg_bool(args, "include_deleted")?.unwrap_or(false);
let doc: Option<String> = if id.is_some() || path.is_some() {
let tomb: Option<String> = if include_deleted {
match (id, path) {
(Some(i), _) => self
.store
.conn()
.query_row(
"SELECT doc_id FROM docs WHERE doc_id = ?1 AND repo_id = ?2",
params![i, repo],
|r| r.get(0),
)
.optional()?,
(None, Some(p)) => self
.store
.conn()
.query_row(
"SELECT doc_id FROM docs WHERE repo_id = ?1 AND path = ?2",
params![repo, storage_path(p)],
|r| r.get(0),
)
.optional()?,
(None, None) => None,
}
} else {
None
};
Some(match tomb {
Some(d) => d,
None => self.locate_doc(&repo, name, args)?,
})
} else {
None
};
history::docs_history(
&self.store,
&repo,
glob,
doc.as_deref(),
include_deleted,
arg_i64(args, "limit")?,
)
}
"changes_since" => {
let (repo, _) = self.scope(args)?;
let page = self.store.changes_since(
&repo,
arg_i64(args, "cursor")?.unwrap_or(0),
arg_usize(args, "limit")?.unwrap_or(50),
arg_str(args, "origin").filter(|o| !o.is_empty()),
)?;
Ok(root_changes_page(page.to_json()))
}
"repos_status" => {
let (repo, root) = self.scope(args)?;
let fs = RealFileSystem;
let disk = root
.as_deref()
.map(|r| (&fs as &dyn omgbase_sync::FileSystem, Path::new(r)));
Ok(omgbase_sync::repos_status(&self.store, &repo, disk)?.to_json())
}
"sync_status" => {
let (repo, root) = self.scope(args)?;
let fs = RealFileSystem;
let disk = root
.as_deref()
.map(|r| (&fs as &dyn omgbase_sync::FileSystem, Path::new(r)));
Ok(omgbase_sync::sync_status(&self.store, &repo, disk)?.to_json())
}
"repos" => {
let rows = self.repo_rows()?;
Ok(json!({
"repos": rows.iter().map(|r| json!({ "slug": r.slug, "hasSource": r.root_path.is_some() })).collect::<Vec<_>>(),
}))
}
"version" => version_info(Some(&self.store), &self.build_info, self.profile),
other => Err(SurfaceError::other(format!("unknown tool {other}"))),
}
}
}
fn block_missing(value: &str) -> SurfaceError {
SurfaceError::with_data(
"block_missing",
format!("no block with id {}", Json::String(value.to_owned())),
json!({ "id": value }),
)
}
fn node_has_no_block(node_id: &str, doc_id: &str) -> SurfaceError {
SurfaceError::with_data(
"block_missing",
format!("node {} has no block", Json::String(node_id.to_owned())),
json!({ "id": node_id, "doc_id": doc_id }),
)
}
pub const PROVIDER_UNAVAILABLE_MESSAGE: &str = "semantic(...) needs an embedding provider; none is configured — set embedding.provider (omg config set embedding.provider <command|url>) and embed the repository (omg embed drain)";
#[must_use]
pub fn provider_unavailable() -> SurfaceError {
SurfaceError::with_data(
"provider_unavailable",
PROVIDER_UNAVAILABLE_MESSAGE,
json!({ "provider": null }),
)
}
fn parse_effects(v: &Json) -> Result<Vec<PublicEffect>> {
PublicEffect::list_from_json(v).map_err(|m| SurfaceError::filter_invalid(m, "arguments"))
}
#[must_use]
pub fn plan_json(plan: &Plan) -> Json {
let mut v = plan.to_json(true);
if let Some(preview) = v.get_mut("preview").and_then(Json::as_object_mut) {
if let Some(revs) = preview.get_mut("revisions").and_then(Json::as_array_mut) {
for r in revs {
root_field(r, "path");
}
}
if let Some(results) = preview.get_mut("results").and_then(Json::as_array_mut) {
for r in results {
if let Some(o) = r.as_object_mut() {
if let Some(m) = o.shift_remove("merged_into") {
o.insert("mergedInto".to_owned(), m);
}
}
}
}
if let Some(diffs) = preview.get_mut("diffs") {
*diffs = reference_keyed(diffs);
}
if let Some(retargets) = preview.get_mut("retarget").and_then(Json::as_array_mut) {
for r in retargets {
if let Some(hits) = r.get_mut("hits").and_then(Json::as_array_mut) {
for h in hits {
root_field(h, "path");
}
}
}
}
}
v
}
#[must_use]
pub fn receipt_json(receipt: &Receipt) -> Json {
let mut v = receipt.to_json();
if let Some(revs) = v.get_mut("revisions").and_then(Json::as_array_mut) {
for r in revs {
root_field(r, "path");
}
}
v
}
fn dangling_json(l: &omgbase_store::InboundLink) -> Json {
let mut m = Map::new();
m.insert("doc_id".to_owned(), json!(l.doc));
m.insert("path".to_owned(), json!(reference_path(&l.path)));
m.insert("block_id".to_owned(), json!(l.block));
m.insert("target".to_owned(), json!(l.target));
m.insert("anchor".to_owned(), json!(l.anchor));
if let Some(f) = &l.field {
m.insert("field".to_owned(), json!(f));
}
Json::Object(m)
}
fn path_taken(message: String) -> SurfaceError {
SurfaceError::from(MutationError::new(ErrorCode::PathTaken, message))
}
fn root_changes_page(mut page: Json) -> Json {
let Some(digests) = page.get_mut("digests").and_then(Json::as_array_mut) else {
return page;
};
for d in digests.iter_mut() {
if let Some(revs) = d.get_mut("revisions").and_then(Json::as_array_mut) {
for r in revs.iter_mut() {
root_field(r, "path");
}
}
if let Some(summary) = d.get("summary").and_then(Json::as_str) {
let rooted = root_summary(summary);
d["summary"] = Json::String(rooted);
}
}
page
}
fn root_summary(summary: &str) -> String {
let Some(colon) = summary.find(": ") else {
return summary.to_owned();
};
let (head, rest) = summary.split_at(colon + 2);
let (subject, detail) = match rest.find(" — ") {
Some(i) => rest.split_at(i),
None => (rest, ""),
};
let root_list = |s: &str| -> String {
if s.is_empty() {
return String::new();
}
s.split(", ")
.map(reference_path)
.collect::<Vec<_>>()
.join(", ")
};
let subject = if let Some(paths) = subject.strip_prefix("deleted ") {
format!("deleted {}", root_list(paths))
} else if let Some(mv) = subject.strip_prefix("moved ") {
let (mv, rest) = match mv.split_once("; ") {
Some((mv, rest)) => (mv, Some(rest)),
None => (mv, None),
};
let moved = match mv.split_once(" → ") {
Some((from, to)) => format!("moved {} → {}", reference_path(from), reference_path(to)),
None => format!("moved {mv}"),
};
match rest {
Some(rest) => format!("{moved}; {}", root_list(rest)),
None => moved,
}
} else {
root_list(subject)
};
format!("{head}{subject}{detail}")
}
fn evidence_json(e: &omgbase_store::Evidence) -> Json {
let mut m = Map::new();
m.insert("rrf".to_owned(), json!(e.rrf));
m.insert("boosts".to_owned(), e.boosts.to_json());
if let Some(r) = e.fts_rank {
m.insert("ftsRank".to_owned(), json!(r));
}
if let Some(r) = e.vector_rank {
m.insert("vectorRank".to_owned(), json!(r));
if let Some(c) = e.cosine {
m.insert("cosine".to_owned(), json!(c));
}
}
Json::Object(m)
}
fn observe_json(o: &omgbase_store::ObserveOutcome) -> Json {
json!({
"id": o.doc_id,
"path": reference_path(&o.path),
"rev": o.rev,
"commitId": o.commit_id,
"converged": o.converged,
"echo": o.echo,
"conflicted": o.conflicted,
"dispositions": o.dispositions.iter().map(|(k, n)| json!({ "kind": k, "count": n })).collect::<Vec<_>>(),
})
}
#[cfg(test)]
mod sugar_tests;
#[cfg(test)]
mod tests {
use super::*;
use omgbase_store::{MemDocStore, SequentialMinter};
fn surface() -> Surface {
let mut store =
Store::open_in_memory_with_minter(Box::new(SequentialMinter::new())).unwrap();
let repo = store.create_repo("fixture").unwrap();
let mut n = 0;
Surface::new(store, &repo, None)
.with_doc_store(Box::new(MemDocStore::new()))
.with_clock(move || {
n += 1;
format!("2026-09-26T10:{n:02}:00.000Z")
})
}
#[test]
fn catalog_lists_every_tool_of_the_table() {
let names: Vec<&str> = tools().iter().map(|t| t.name).collect();
for want in [
"docs_outline",
"docs_read",
"docs_read_many",
"blocks_read",
"nodes_block_read",
"blocks_read_many",
"docs_tree",
"docs_list",
"query_syntax",
"query",
"graph",
"blocks_search",
"blocks_resolve",
"apply",
"plan",
"execute",
"blocks_insert",
"blocks_update",
"blocks_move",
"blocks_remove",
"blocks_split",
"blocks_merge",
"blocks_check",
"nodes_set",
"blocks_append",
"docs_append",
"links_retarget",
"links_stale",
"links_repair",
"docs_create",
"docs_move",
"docs_delete",
"docs_set_meta",
"docs_update",
"observe",
"observe_many",
"observe_delete",
"blocks_history",
"docs_diff",
"docs_diff_unified",
"docs_read_at",
"docs_history",
"changes_since",
"repos_status",
"sync_status",
"repos",
"version",
] {
assert!(names.contains(&want), "missing {want}");
}
assert_eq!(names.len(), 47);
for t in tools() {
assert_eq!(t.input_schema["type"], "object");
}
}
#[test]
fn profiles_filter_the_listing_not_the_semantics() {
let full = surface();
assert_eq!(full.profile(), Profile::Full);
assert_eq!(full.tools(), tools());
let minimal = surface().with_profile(Profile::Minimal);
assert_eq!(minimal.profile(), Profile::Minimal);
let names: Vec<&str> = minimal.tools().iter().map(|t| t.name).collect();
let mut sorted_names = names.clone();
sorted_names.sort_unstable();
let mut sorted_set = MINIMAL_TOOLS.to_vec();
sorted_set.sort_unstable();
assert_eq!(sorted_names, sorted_set);
assert_eq!(
names,
[
"docs_read",
"docs_tree",
"query_syntax",
"query",
"blocks_resolve",
"apply",
"plan",
"execute",
"changes_since",
"version",
]
);
for t in minimal.tools() {
assert_eq!(
Some(&t),
tool_spec(t.name),
"{} is the full table's spec",
t.name
);
}
assert!(
Surface::is_write_tool("docs_create"),
"the write set is static"
);
assert_eq!("full".parse::<Profile>(), Ok(Profile::Full));
assert_eq!("minimal".parse::<Profile>(), Ok(Profile::Minimal));
assert_eq!(
"lite".parse::<Profile>(),
Err("unknown tools profile \"lite\" (expected full|minimal)".to_owned())
);
assert_eq!(Profile::Minimal.to_string(), "minimal");
let mut minimal = minimal;
let v = minimal.call("version", json!({}));
assert_eq!(v.body["profile"], "minimal");
let mut full = full;
assert_eq!(full.call("version", json!({})).body["profile"], "full");
let listed = minimal.call("docs_list", json!({}));
assert!(!listed.is_error);
let unknown = minimal.call("docs_lits", json!({}));
assert!(unknown.is_error);
assert_eq!(unknown.body["message"], "unknown tool docs_lits");
}
#[test]
fn plan_apply_and_execute_take_a_selector() {
let mut s = surface();
for (path, content) in [
("hub.md", "---\ntype: project\n---\n\n# Hub\n\nIntro.\n"),
(
"ws/alpha.md",
"---\ntype: workstream\nstatus: completed\nproject: hub.md\n---\n\n# Alpha\n",
),
(
"ws/beta.md",
"---\ntype: workstream\nstatus: completed\nproject: hub.md\n---\n\n# Beta\n",
),
(
"ws/gamma.md",
"---\ntype: workstream\nstatus: active\nproject: hub.md\n---\n\n# Gamma\n",
),
] {
let out = s.call("observe", json!({ "path": path, "content": content }));
assert!(!out.is_error, "{}", out.body);
}
let select = "from docs where type == \"workstream\" and status == \"completed\"";
let templates = json!([
{ "op": "insert", "into": { "kind": "document", "path": { "oqx": "project" } }, "markdown": { "oqx": "\"- [\" + title + \"](\" + path + \")\"" } },
{ "op": "set", "field": "frontmatter.rolled_up", "value": true },
]);
let plan = s.call(
"plan",
json!({ "select": select, "effects": templates, "diffs": true }),
);
assert!(!plan.is_error, "{}", plan.body);
let plan = plan.body;
assert_eq!(plan["planner_v"], "p3");
assert_eq!(plan["effects"].as_array().map(Vec::len), Some(4));
assert_eq!(
plan["effects"][0]["into"],
json!({ "kind": "document", "id": "d_0", "path": "hub.md" })
);
assert_eq!(plan["effects"][0]["markdown"], "- [Alpha](/ws/alpha.md)");
assert_eq!(
plan["effects"][1]["target"],
json!({ "kind": "document", "id": "d_1", "path": "ws/alpha.md" })
);
assert_eq!(
plan["selection"],
json!({ "query": select, "templates": templates, "cardinality": 2, "identities": ["d_1", "d_2"] })
);
assert!(
plan["preview"]["diffs"]["/hub.md"]["after"]
.as_str()
.unwrap()
.contains("- [Beta](/ws/beta.md)")
);
let receipt = s.call(
"execute",
json!({ "plan": plan.clone(), "reason": "rollup" }),
);
assert!(!receipt.is_error, "{}", receipt.body);
assert_eq!(receipt.body["plan_id"], plan["plan_id"]);
assert_eq!(receipt.body["revisions"].as_array().map(Vec::len), Some(3));
let ops: String = s
.store()
.conn()
.query_row(
"SELECT ops FROM commits WHERE commit_id = ?1",
[receipt.body["commit"].as_str().unwrap()],
|r| r.get(0),
)
.unwrap();
assert!(ops.contains("\"selection\":{\"cardinality\":2"), "{ops}");
let again = s.call(
"plan",
json!({ "select": select, "effects": templates, "expect": { "max": 5 } }),
);
assert!(!again.is_error, "{}", again.body);
assert_eq!(again.body["selection"]["expect"], json!({ "max": 5 }));
let out = s.call("observe", json!({ "path": "ws/delta.md", "content": "---\ntype: workstream\nstatus: completed\nproject: hub.md\n---\n\n# Delta\n" }));
assert!(!out.is_error);
let stale = s.call("execute", json!({ "plan_id": again.body["plan_id"], "select": select, "effects": templates, "expect": { "max": 5 } }));
assert!(stale.is_error);
assert_eq!(stale.body["error"], "plan_mismatch", "{}", stale.body);
assert_eq!(stale.body["data"]["expected"], again.body["plan_id"]);
let unbounded = s.call("apply", json!({ "select": select, "effects": templates }));
assert!(unbounded.is_error);
assert_eq!(unbounded.body["error"], "filter_invalid");
assert_eq!(unbounded.body["data"]["hint"], "selection");
assert_eq!(
unbounded.body["message"],
"`expect.max` is required on `apply` with `select` — bound the selection, or preview it with `plan`"
);
let small = s.call(
"apply",
json!({ "select": select, "effects": templates, "expect": { "max": 2 } }),
);
assert!(small.is_error);
assert_eq!(small.body["error"], "selection_too_large", "{}", small.body);
assert_eq!(
small.body["data"],
json!({
"cardinality": 3,
"max": 2,
"fix": omgbase_mutate::canonical_json(&json!({ "select": select, "effects": templates, "expect": { "max": 3 } })),
})
);
let one = s.call(
"apply",
json!({ "select": select, "effects": templates, "expect": { "max": 1 } }),
);
assert_eq!(
one.body["data"],
json!({
"cardinality": ">1",
"max": 1,
"fix": omgbase_mutate::canonical_json(&json!({ "select": select, "effects": templates, "expect": { "max": 3 } })),
}),
"{}",
one.body
);
let none = s.call("apply", json!({ "select": "from docs where type == \"nope\"", "effects": templates, "expect": { "max": 10 } }));
assert!(!none.is_error, "{}", none.body);
assert_eq!(none.body["commit"], Json::Null);
assert_eq!(none.body["revisions"], json!([]));
let empty = s.call(
"plan",
json!({ "select": "from docs where type == \"nope\"", "effects": templates }),
);
assert_eq!(empty.body["effects"], json!([]));
assert_eq!(empty.body["selection"]["cardinality"], 0);
let e = s.call(
"plan",
json!({ "effects": templates, "expect": { "max": 1 } }),
);
assert_eq!(
e.body["message"],
"`expect` bounds a selection — pass `select` with it"
);
let e = s.call(
"plan",
json!({ "select": "from links", "effects": templates }),
);
assert_eq!(e.body["error"], "filter_invalid");
assert_eq!(e.body["data"]["hint"], "selection");
assert!(
e.body["message"]
.as_str()
.unwrap()
.starts_with("select: `links` rows")
);
let e = s.call("execute", json!({ "plan": plan, "select": select }));
assert!(
e.body["message"]
.as_str()
.unwrap()
.starts_with("pass exactly one of `plan` or `plan_id` + `effects`"),
"{}",
e.body
);
let e = s.call(
"plan",
json!({ "select": select, "effects": templates, "expect": 3 }),
);
assert!(
e.body["message"]
.as_str()
.unwrap()
.starts_with("`expect` must be"),
"{}",
e.body
);
}
#[test]
fn plan_execute_and_apply_effects_round_trip() {
let mut s = surface();
let out = s.call(
"observe",
json!({ "path": "a.md", "content": "---\nstatus: open\n---\n\n# A\n\nAlpha.\n" }),
);
assert!(!out.is_error, "{}", out.body);
let effects = json!([
{ "op": "set", "target": { "kind": "document", "path": "/a.md" }, "field": "frontmatter.status", "value": "done" },
{ "op": "insert", "into": { "kind": "section", "heading_id": "b_0" }, "markdown": "Tail." },
]);
let plan = s.call("plan", json!({ "effects": effects, "diffs": true }));
assert!(!plan.is_error, "{}", plan.body);
let plan = plan.body;
assert_eq!(plan["version"], 1);
assert_eq!(plan["plan_id"].as_str().map(str::len), Some(64));
assert_eq!(
plan["effects"][0]["target"],
json!({ "kind": "document", "id": "d_0", "path": "a.md" }),
"the core keeps storage paths"
);
assert_eq!(
plan["preview"]["revisions"][0]["path"], "/a.md",
"the preview is rooted"
);
assert!(
plan["preview"]["diffs"].get("/a.md").is_some(),
"{}",
plan["preview"]["diffs"]
);
assert_eq!(
plan["new_ids"],
json!([{ "symbol": "$new.0", "kind": "block" }])
);
let dry = s.call("apply", json!({ "effects": effects, "dry_run": true }));
assert_eq!(dry.body["error"], "filter_invalid", "{}", dry.body);
assert_eq!(
dry.body["message"],
"`dry_run` is no longer an argument — preview with `plan { effects: [...] }` (apply takes effects, expect?, reason?, repo?, select?)"
);
let ops = s.call("apply", json!({ "ops": [] }));
assert_eq!(ops.body["error"], "filter_invalid");
assert_eq!(
ops.body["message"],
"`ops` is no longer an argument — `apply` takes `effects` (the kernel ops are a library/fixture contract since spec/mutate 3.0) (apply takes effects, expect?, reason?, repo?, select?)"
);
assert_eq!(ops.body["data"]["hint"], "arguments");
let neither = s.call("apply", json!({}));
assert_eq!(neither.body["error"], "filter_invalid");
assert_eq!(
neither.body["message"],
"`effects` is required (apply takes effects, expect?, reason?, repo?, select?)"
);
let previewed = s
.preview_call("apply", &json!({ "effects": effects }))
.unwrap();
assert_eq!(previewed["plan_id"], plan["plan_id"]);
assert!(previewed.get("preview").is_some());
let bad = s.call("apply", json!({ "effects": [{ "op": "nope" }] }));
assert!(bad.is_error);
assert_eq!(bad.body["error"], "filter_invalid");
assert_eq!(bad.body["data"]["hint"], "arguments");
assert!(
bad.body["message"]
.as_str()
.unwrap()
.starts_with("effects[0]:"),
"{}",
bad.body
);
let empty = s.call("execute", json!({}));
assert!(empty.is_error);
assert_eq!(empty.body["data"]["hint"], "arguments");
let mut echoed = plan.clone();
echoed.as_object_mut().unwrap().remove("preview");
let receipt = s.call("execute", json!({ "plan": echoed, "reason": "slice 1" }));
assert!(!receipt.is_error, "{}", receipt.body);
assert_eq!(receipt.body["plan_id"], plan["plan_id"]);
assert_eq!(receipt.body["committed"], true);
assert_eq!(receipt.body["commit"], "c_1");
assert_eq!(receipt.body["revisions"][0]["path"], "/a.md");
assert_eq!(receipt.body["new_ids"], json!({ "$new.0": "b_2" }));
let read = s.call("docs_read", json!({ "path": "a.md" }));
assert_eq!(
read.body["raw"],
"---\nstatus: done\n---\n\n# A\n\nAlpha.\n\nTail.\n"
);
let again = s.call("execute", json!({ "plan": echoed }));
assert!(again.is_error);
assert_eq!(again.body["error"], "stale_plan");
assert_eq!(again.body["retriable"], true);
let digest = s.call(
"execute",
json!({ "plan_id": plan["plan_id"], "effects": effects }),
);
assert!(digest.is_error);
assert_eq!(digest.body["error"], "plan_mismatch", "{}", digest.body);
let half = s.call("execute", json!({ "plan_id": plan["plan_id"] }));
assert!(half.is_error);
assert!(
half.body["message"]
.as_str()
.unwrap()
.starts_with("the digest form needs both"),
"{}",
half.body
);
let v2 = s.call(
"execute",
json!({ "plan": { "version": 2, "plan_id": "x" } }),
);
assert_eq!(v2.body["error"], "unsupported_plan_version", "{}", v2.body);
let ops_count: i64 = s
.store()
.conn()
.query_row(
"SELECT count(*) FROM commits WHERE ops IS NOT NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(ops_count, 1, "the effect commit recorded its intent");
assert!(Surface::is_write_tool("plan") && Surface::is_write_tool("execute"));
}
#[test]
fn bare_hits_are_the_projection_only() {
let mut s = surface();
for (path, content) in [
("a.md", "---\nlayer: canon\nid: authored-a\n---\n# Alpha\n"),
("b.md", "---\nlayer: canon\n---\n# Beta\n"),
("c.md", "---\nlayer: working\n---\n# Gamma\n"),
] {
let out = s.call("observe", json!({ "path": path, "content": content }));
assert!(!out.is_error, "{}", out.body);
}
let plain = s.call("query", json!({ "query": "select title from docs" }));
assert_eq!(
plain.body["hits"][0],
json!({ "id": "d_0", "path": "/a.md", "title": "Alpha" })
);
let bare = s.call(
"query",
json!({ "query": "select title from docs", "bare": true }),
);
assert!(!bare.is_error, "{}", bare.body);
assert_eq!(
bare.body["hits"],
json!([{ "title": "Alpha" }, { "title": "Beta" }, { "title": "Gamma" }])
);
assert_eq!(bare.body["truncated"], false);
assert_eq!(bare.body["cursor"], Json::Null);
assert_eq!(bare.body["consumer"], "collect");
let intrinsic = s.call(
"query",
json!({ "query": "select i: data.id, id, path from docs where path == \"a.md\"", "bare": true }),
);
assert_eq!(
intrinsic.body["hits"].to_string(),
r#"[{"i":"authored-a","id":"d_0","path":"/a.md"}]"#
);
let no_collision = s.call(
"query",
json!({ "query": "select path: layer from docs where title == \"Alpha\"", "bare": true }),
);
assert_eq!(no_collision.body["hits"], json!([{ "path": "canon" }]));
assert_eq!(
s.call("query", json!({ "query": "select path: layer from docs" }))
.body["error"],
"filter_invalid"
);
let count = s.call("query", json!({ "query": "docs count { }", "bare": true }));
assert_eq!(count.body["count"], 3, "{}", count.body);
let page1 = s.call(
"query",
json!({ "query": "select title from docs", "bare": true, "limit": 2 }),
);
assert_eq!(page1.body["truncated"], true);
assert_eq!(
page1.body["hits"],
json!([{ "title": "Alpha" }, { "title": "Beta" }])
);
let cursor = page1.body["cursor"].as_str().expect("a cursor").to_owned();
let page2 = s.call(
"query",
json!({ "query": "select title from docs", "bare": true, "limit": 2, "cursor": cursor }),
);
assert_eq!(page2.body["hits"], json!([{ "title": "Gamma" }]));
assert_eq!(page2.body["truncated"], false);
let first = s.call(
"query",
json!({ "query": "docs first { select layer where layer == \"working\" }", "bare": true }),
);
assert_eq!(first.body["hits"], json!([{ "layer": "working" }]));
let distinct = s.call(
"query",
json!({ "query": "select distinct layer from docs", "bare": true }),
);
assert_eq!(
distinct.body["hits"],
json!([{ "layer": "canon" }, { "layer": "working" }])
);
let nested = s.call(
"query",
json!({ "query": "select b: blocks collect { } from docs where path == \"b.md\"", "bare": true }),
);
assert_eq!(
nested.body["hits"],
json!([{ "b": [{ "id": "b_1", "path": "/b.md" }] }])
);
let no_select = s.call("query", json!({ "query": "from docs", "bare": true }));
assert_eq!(no_select.body["error"], "filter_invalid");
assert_eq!(
no_select.body["message"],
"`bare` needs a `select` — without a projection a hit is only its envelope"
);
let values = s.call(
"query",
json!({ "query": "select title values from docs", "bare": true }),
);
assert_eq!(values.body["error"], "filter_invalid");
assert_eq!(
values.body["message"],
crate::query::BARE_WITH_VALUES_MESSAGE
);
let typed = s.call(
"query",
json!({ "query": "select title from docs", "bare": "yes" }),
);
assert_eq!(typed.body["error"], "filter_invalid");
assert_eq!(
typed.body["message"],
"`bare` must be a boolean (query takes bare?, cursor?, limit?, query, repo?)"
);
let off = s.call(
"query",
json!({ "query": "select title from docs", "bare": false }),
);
assert_eq!(off.body["hits"][0]["id"], "d_0");
assert!(s.tools().iter().any(
|t| t.name == "query" && t.input_schema["properties"]["bare"]["type"] == "boolean"
));
}
#[test]
fn observe_read_and_query_round_trip() {
let mut s = surface();
let out = s.call("observe", json!({ "path": "a.md", "content": "---\nlayer: canon\n---\n# Title\n\nHello world.\n\n- [ ] task one\n" }));
assert!(!out.is_error, "{}", out.body);
assert_eq!(out.body["id"], "d_0");
assert_eq!(out.body["echo"], false);
let read = s.call("docs_read", json!({ "path": "a.md", "include_ids": true }));
assert!(!read.is_error, "{}", read.body);
assert_eq!(read.body["path"], "/a.md");
assert_eq!(
s.call("docs_read", json!({ "path": "/a.md" })).body["path"],
"/a.md"
);
let retired = s.call("docs_read", json!({ "doc": "a.md" }));
assert_eq!(retired.body["error"], "filter_invalid");
assert_eq!(
retired.body["message"],
"`doc` is no longer an argument — pass `id` or `path` (docs_read takes at?, id?, include_ids?, path?, repo?)"
);
let neither = s.call("docs_read", json!({}));
assert_eq!(
neither.body["message"],
"pass id or path (docs_read takes at?, id?, include_ids?, path?, repo?)"
);
assert_eq!(
s.call("docs_read", json!({ "path": "//a.md" })).body["id"],
"d_0"
);
assert_eq!(read.body["properties"]["frontmatter"]["layer"], "canon");
assert!(read.body["ids"].as_array().unwrap().len() >= 3);
let q = s.call("query", json!({ "query": "select title, layer, t: nodes collect { value where kind == \"md:task\" } from docs where layer == \"canon\" && nodes count { where kind == \"md:task\" } == 1" }));
assert!(!q.is_error, "{}", q.body);
assert_eq!(q.body["hits"][0]["title"], "Title");
assert_eq!(q.body["hits"][0]["layer"], "canon");
assert_eq!(q.body["hits"][0]["t"][0]["value"], "task one");
assert_eq!(q.body["hits"][0]["id"], "d_0");
assert_eq!(q.body["hits"][0]["path"], "/a.md");
assert_eq!(q.body["consumer"], "collect");
let c = s.call(
"query",
json!({ "query": "blocks count { where text(\"hello\") }" }),
);
assert_eq!(c.body["count"], 1);
let ok = s.call(
"query",
json!({ "query": "from docs where path == \"a.md\"" }),
);
assert!(!ok.is_error, "{}", ok.body);
assert_eq!(ok.body["hits"][0]["id"], "d_0");
let bad = s.call("query", json!({ "query": "select data.path from docs" }));
assert!(bad.is_error);
assert_eq!(bad.body["error"], "filter_invalid");
assert_eq!(
bad.body["message"],
"projection `path` collides with the hit envelope — alias it (`p: data.path`)"
);
let sem = s.call(
"query",
json!({ "query": "from docs where semantic(\"x\") > 0.5" }),
);
assert_eq!(sem.body["error"], "provider_unavailable");
assert_eq!(sem.body["data"], json!({ "provider": null }));
let outline = s.call("docs_outline", json!({ "path": "a.md" }));
let text = outline.body["text"].as_str().unwrap();
assert!(text.starts_with("b_0 h1 Title §"), "{text}");
assert!(text.contains("☐ task one"));
let missing = s.call("docs_read", json!({ "path": "nope.md" }));
assert_eq!(missing.body["error"], "doc_missing");
assert_eq!(
missing.body["message"],
"no document with id or path \"nope.md\""
);
assert_eq!(missing.body["data"], json!({ "path": "nope.md" }));
let repos = s.call("repos", json!({}));
assert_eq!(repos.body["repos"][0]["slug"], "fixture");
let unknown = s.call("docs_list", json!({ "repo": "zzz" }));
assert_eq!(unknown.body["error"], "repo_not_found");
}
#[test]
fn insert_and_move_carry_the_destination_parent_cas_and_many_reads_keep_one_item() {
let mut s = surface();
s.call(
"observe",
json!({ "path": "a.md", "content": "# T\n\nOne.\n\nTwo.\n" }),
);
s.call("observe", json!({ "path": "b.md", "content": "# U\n" }));
let stale = s.call(
"blocks_insert",
json!({ "to": "a.md", "markdown": "Three.", "expect": { "parent_children_hash": "00" } }),
);
assert!(stale.is_error, "{}", stale.body);
assert_eq!(stale.body["error"], "stale_expectation");
assert_eq!(stale.body["retriable"], true);
let current = stale.body["data"]["current"]["parent_children_hash"]
.as_str()
.expect("current hash")
.to_owned();
assert_eq!(current.len(), 64, "{current}");
let ok = s.call(
"blocks_insert",
json!({ "to": "a.md", "markdown": "Three.", "expect": { "parent_children_hash": current, "content_hash": "dropped, not checked" } }),
);
assert!(!ok.is_error, "{}", ok.body);
assert_eq!(ok.body["committed"], true);
let read = s.call("docs_read", json!({ "path": "a.md" }));
assert_eq!(read.body["raw"], "# T\n\nOne.\n\nTwo.\n\nThree.\n");
let stale = s.call(
"blocks_move",
json!({ "blocks": ["b_1", "b_2"], "to": "a.md", "at": "end", "expect": { "parent_children_hash": "00" } }),
);
assert_eq!(stale.body["error"], "stale_expectation", "{}", stale.body);
let current = stale.body["data"]["current"]["parent_children_hash"]
.as_str()
.expect("current hash")
.to_owned();
let ok = s.call(
"blocks_move",
json!({ "blocks": ["b_1", "b_2"], "to": "a.md", "at": "end", "expect": { "parent_children_hash": current } }),
);
assert!(!ok.is_error, "{}", ok.body);
let read = s.call("docs_read", json!({ "path": "a.md" }));
assert_eq!(read.body["raw"], "# T\n\nThree.\n\nOne.\n\nTwo.\n\n");
let dry = s.call(
"plan",
json!({ "effects": [{ "op": "move", "targets": [{ "kind": "block", "id": "b_1" }], "into": { "kind": "document", "path": "a.md" }, "at": "start", "expect": { "parent_children_hash": "00" } }] }),
);
assert_eq!(dry.body["error"], "stale_expectation", "{}", dry.body);
let dry = s
.preview_call(
"blocks_move",
&json!({ "blocks": ["b_1"], "to": "a.md", "at": "start", "expect": { "parent_children_hash": "00" } }),
)
.unwrap_err();
assert_eq!(dry.code, "stale_expectation");
let many = s.call(
"docs_read_many",
json!({ "docs": ["a.md", "b.md"], "budget_tokens": 1 }),
);
assert_eq!(
many.body["items"].as_array().unwrap().len(),
1,
"{}",
many.body
);
assert_eq!(many.body["items"][0]["path"], "/a.md");
assert_eq!(many.body["truncated"], true);
let one = s.call(
"docs_read_many",
json!({ "docs": ["b.md"], "budget_tokens": 1 }),
);
assert_eq!(one.body["items"].as_array().unwrap().len(), 1);
assert_eq!(one.body["truncated"], false);
let nodes = s.call(
"blocks_read_many",
json!({ "ids": ["b_zzz", "b_1", "b_2"], "resolution": "full", "budget_tokens": 1 }),
);
assert_eq!(
nodes.body["nodes"].as_array().unwrap().len(),
1,
"{}",
nodes.body
);
assert_eq!(nodes.body["nodes"][0]["id"], "b_1");
assert_eq!(nodes.body["truncated"], true);
assert_eq!(nodes.body["unresolved"], json!(["b_zzz"]));
}
#[test]
fn writes_go_through_the_fixed_doc_store_and_fire_the_hook() {
use std::cell::Cell;
use std::rc::Rc;
let fired = Rc::new(Cell::new(0));
let f2 = Rc::clone(&fired);
let mut s = surface().with_mutation_hook(move || f2.set(f2.get() + 1));
s.call(
"observe",
json!({ "path": "a.md", "content": "# T\n\nOne.\n" }),
);
assert_eq!(fired.get(), 1);
let dry = s
.preview_call(
"blocks_insert",
&json!({ "to": "a.md", "markdown": "Two." }),
)
.unwrap();
assert!(dry.get("committed").is_none(), "a Plan: {dry}");
assert_eq!(
dry["new_ids"],
json!([{ "symbol": "$new.0", "kind": "block" }])
);
assert_eq!(fired.get(), 1, "a preview never fires the hook");
let plan = s.call("plan", json!({ "effects": [{ "op": "insert", "into": { "kind": "document", "path": "a.md" }, "markdown": "Two." }] }));
assert_eq!(
plan.body["plan_id"], dry["plan_id"],
"the preview IS the plan"
);
assert_eq!(fired.get(), 1, "`plan` never fires the hook");
let wet = s.call("blocks_insert", json!({ "to": "a.md", "markdown": "Two." }));
assert!(!wet.is_error, "{}", wet.body);
assert_eq!(fired.get(), 2);
assert_eq!(wet.body["plan_id"], dry["plan_id"]);
assert_eq!(wet.body["committed"], true);
assert_eq!(wet.body["commit"], "c_1");
assert_eq!(wet.body["new_ids"], json!({ "$new.0": "b_2" }));
assert_eq!(wet.body["revisions"][0]["path"], "/a.md");
assert!(wet.body.get("results").is_none() && wet.body.get("diffs").is_none());
let read = s.call("docs_read", json!({ "id": "d_0" }));
assert_eq!(read.body["raw"], "# T\n\nOne.\n\nTwo.\n");
let upd = s.call(
"blocks_update",
json!({ "id": "b_1", "markdown": "One, edited." }),
);
assert!(!upd.is_error, "{}", upd.body);
assert!(
upd.body.get("id").is_none() && upd.body.get("ids").is_none(),
"{}",
upd.body
);
assert_eq!(upd.body["revisions"].as_array().map(Vec::len), Some(1));
let attrs = s.call(
"blocks_update",
json!({ "id": "b_1", "attrs": { "checked": true } }),
);
assert_eq!(
attrs.body["message"],
"`attrs` is no longer an argument — pass `checked` (blocks_update takes checked?, expect?, id, markdown?, repo?)"
);
let dry = s.call(
"blocks_update",
json!({ "id": "b_1", "markdown": "x", "dry_run": true }),
);
assert_eq!(
dry.body["message"],
"`dry_run` is no longer an argument — preview with `plan { effects: [...] }` (blocks_update takes checked?, expect?, id, markdown?, repo?)"
);
let nothing = s.call("blocks_update", json!({ "id": "b_1" }));
assert_eq!(
nothing.body["message"],
"pass `markdown` and/or `checked` (blocks_update takes checked?, expect?, id, markdown?, repo?)"
);
let app = s.call(
"docs_append",
json!({ "path": "a.md", "section": "T", "text": "Three." }),
);
assert!(!app.is_error, "{}", app.body);
let amb = s.call(
"docs_append",
json!({ "path": "a.md", "section": "Nope", "text": "x" }),
);
assert_eq!(amb.body["error"], "parent_missing");
assert_eq!(amb.body["message"], "no heading \"Nope\" in /a.md");
let into = s.call("blocks_append", json!({ "id": "b_0", "text": "Four." }));
assert!(!into.is_error, "{}", into.body);
let leaf = s.call("blocks_append", json!({ "id": "b_1", "text": "x" }));
assert_eq!(leaf.body["error"], "block_not_container", "{}", leaf.body);
assert_eq!(
leaf.body["data"],
json!({ "id": "b_1", "type": "paragraph", "fix": "blocks_insert {\"at\":{\"after\":\"b_1\"},\"markdown\":\"x\",\"to\":\"/a.md\"}" })
);
let hist = s.call("blocks_history", json!({ "id": "b_1" }));
let entries = hist.body.as_array().unwrap();
assert!(entries.len() >= 2, "{}", hist.body);
assert_eq!(entries[0]["origin"], "api", "newest first");
let du = s.call("docs_diff_unified", json!({ "path": "a.md" }));
assert!(!du.is_error, "{}", du.body);
assert!(du.body["diff"].as_str().unwrap().contains("+Four."));
}
#[test]
fn doc_tools_return_receipts_and_preview_through_plan() {
use std::cell::Cell;
use std::rc::Rc;
let fired = Rc::new(Cell::new(0));
let f2 = Rc::clone(&fired);
let mut s = surface().with_mutation_hook(move || f2.set(f2.get() + 1));
let b_src = "# B\n\nTarget.\n";
let a_src = "# A\n\nSee [b](b.md).\n";
let b = s.call("docs_create", json!({ "path": "b.md", "markdown": b_src }));
assert!(!b.is_error, "{}", b.body);
assert_eq!(
b.body,
json!({ "plan_id": b.body["plan_id"], "commit": "c_0", "revisions": [{ "id": "d_0", "path": "/b.md", "hash": b.body["revisions"][0]["hash"] }], "new_ids": { "$new.0": "d_0", "$new.1": "b_0", "$new.2": "b_1" }, "committed": true, "summary": ["create /b.md (2 blocks)"] }),
"the created document is revisions[0], its symbols the document then its blocks"
);
let a = s.call("docs_create", json!({ "path": "a.md", "markdown": a_src }));
assert_eq!(a.body["new_ids"]["$new.0"], "d_1", "{}", a.body);
assert_eq!(fired.get(), 2);
let count = |s: &Surface, table: &str| -> i64 {
s.store
.conn()
.query_row(&format!("SELECT count(*) FROM {table}"), [], |r| r.get(0))
.unwrap()
};
let snapshot = |s: &Surface| {
(
count(s, "docs"),
count(s, "commits"),
count(s, "revisions"),
count(s, "blocks"),
)
};
let before = snapshot(&s);
let keys = |v: &Json| -> Vec<String> { v.as_object().unwrap().keys().cloned().collect() };
let c = s
.preview_call(
"docs_create",
&json!({ "path": "c.md", "markdown": "# C", "frontmatter": { "title": "C" } }),
)
.unwrap();
assert_eq!(
c["preview"]["diffs"],
json!({ "/c.md": { "before": "", "after": "---\ntitle: C\n---\n\n# C\n" } })
);
assert_eq!(c["preview"]["revisions"][0]["id"], "$new.0");
assert_eq!(
c["new_ids"],
json!([{ "symbol": "$new.0", "kind": "document" }, { "symbol": "$new.1", "kind": "block" }])
);
let m = s
.preview_call(
"docs_move",
&json!({ "path": "b.md", "to_path": "notes/b.md", "retarget_inbound": true }),
)
.unwrap();
assert_eq!(
keys(&m["preview"]["diffs"]),
["/a.md", "/b.md", "/notes/b.md"]
);
assert_eq!(
m["preview"]["diffs"]["/a.md"],
json!({ "before": a_src, "after": "# A\n\nSee [b](notes/b.md).\n" })
);
assert_eq!(
m["preview"]["diffs"]["/b.md"],
json!({ "before": b_src, "after": "" })
);
assert_eq!(
m["preview"]["diffs"]["/notes/b.md"],
json!({ "before": "", "after": b_src })
);
assert!(
m.get("dangling").is_none(),
"a preview is the Plan alone: {m}"
);
let plain = s
.preview_call(
"docs_move",
&json!({ "path": "b.md", "to_path": "notes/b.md", "retarget_inbound": false }),
)
.unwrap();
assert_eq!(keys(&plain["preview"]["diffs"]), ["/b.md", "/notes/b.md"]);
let d = s
.preview_call("docs_delete", &json!({ "path": "a.md" }))
.unwrap();
assert_eq!(
d["preview"]["diffs"],
json!({ "/a.md": { "before": a_src, "after": "" } })
);
let sm = s
.preview_call(
"docs_set_meta",
&json!({ "path": "a.md", "set": { "status": "open" } }),
)
.unwrap();
assert_eq!(
sm["preview"]["diffs"],
json!({ "/a.md": { "before": a_src, "after": "---\nstatus: open\n---\n\n# A\n\nSee [b](b.md).\n" } })
);
assert_eq!(fired.get(), 2, "previews never fire the mutation hook");
assert_eq!(snapshot(&s), before);
let taken = s.preview_call("docs_create", &json!({ "path": "a.md", "markdown": "x" }));
assert_eq!(taken.unwrap_err().code, "path_taken");
let missing = s.preview_call("docs_delete", &json!({ "path": "nope.md" }));
assert_eq!(missing.unwrap_err().code, "doc_missing");
let dry = s.call("docs_delete", json!({ "path": "a.md", "dry_run": true }));
assert_eq!(
dry.body["message"],
"`dry_run` is no longer an argument — preview with `plan { effects: [...] }` (docs_delete takes id?, path?, repo?)"
);
let real = s.call(
"docs_create",
json!({ "path": "c.md", "markdown": "# C\n" }),
);
assert_eq!(
real.body["new_ids"]["$new.0"], "d_2",
"the preview burned no `d`"
);
assert_eq!(fired.get(), 3);
let moved = s.call(
"docs_move",
json!({ "path": "b.md", "to_path": "notes/b.md" }),
);
assert!(!moved.is_error, "{}", moved.body);
assert_eq!(
keys(&moved.body),
[
"plan_id",
"commit",
"revisions",
"new_ids",
"committed",
"summary",
"retargeted",
"dangling"
]
);
assert_eq!(
moved.body["revisions"],
json!([{ "id": "d_1", "path": "/a.md", "hash": moved.body["revisions"][0]["hash"] }])
);
assert_eq!(moved.body["dangling"], json!([]));
assert_eq!(
moved.body["retargeted"],
json!({ "blocks": ["b_3"], "docs": ["d_1"] })
);
assert_eq!(
s.call("docs_read", json!({ "id": "d_0" })).body["path"],
"/notes/b.md"
);
let back = s.call(
"docs_move",
json!({ "path": "notes/b.md", "to_path": "b.md", "retarget_inbound": false }),
);
assert_eq!(back.body["revisions"], json!([]), "{}", back.body);
assert_eq!(
back.body["dangling"],
json!([{ "doc_id": "d_1", "path": "/a.md", "block_id": "b_3", "target": "notes/b.md", "anchor": null }])
);
assert_eq!(back.body["retargeted"], Json::Null);
let sm = s.call(
"docs_set_meta",
json!({ "path": "a.md", "set": { "status": "open" } }),
);
assert_eq!(
sm.body["revisions"].as_array().map(Vec::len),
Some(1),
"{}",
sm.body
);
assert_eq!(sm.body["committed"], true);
let d = s.call("docs_delete", json!({ "path": "a.md" }));
assert_eq!(d.body["revisions"], json!([]), "{}", d.body);
assert!(d.body["commit"].is_string());
assert_eq!(
s.call("docs_read", json!({ "path": "a.md" })).body["error"],
"doc_missing"
);
}
#[test]
fn change_summaries_are_rooted_like_the_reference_renders_them() {
assert_eq!(
root_summary("observed: a.md, b/c.md — 3 inserted, 1 edited"),
"observed: /a.md, /b/c.md — 3 inserted, 1 edited"
);
assert_eq!(
root_summary("observed: deleted a.md"),
"observed: deleted /a.md"
);
assert_eq!(
root_summary("api(human:spec): moved a.md → b/a.md"),
"api(human:spec): moved /a.md → /b/a.md"
);
assert_eq!(
root_summary("api(human:spec): moved a.md → b/a.md; index.md, c.md — 31 edited"),
"api(human:spec): moved /a.md → /b/a.md; /index.md, /c.md — 31 edited"
);
assert_eq!(root_summary("api(?): x.md"), "api(?): /x.md");
assert_eq!(root_summary("observed: "), "observed: ");
assert_eq!(root_summary("nonsense"), "nonsense");
let page = root_changes_page(json!({
"digests": [{ "summary": "observed: a.md", "revisions": [{ "id": "d_0", "path": "a.md", "hash": "ff" }] }],
"cursor": 1, "truncated": false, "head": 1
}));
assert_eq!(page["digests"][0]["revisions"][0]["path"], "/a.md");
assert_eq!(page["digests"][0]["summary"], "observed: /a.md");
}
#[test]
fn plan_returns_diffs_only_on_request() {
let mut s = surface();
let out = s.call(
"observe",
json!({ "path": "a.md", "content": "# A\n\nOne.\n" }),
);
assert!(!out.is_error, "{}", out.body);
let effects = json!([{ "op": "insert", "into": "/a.md", "markdown": "Two." }]);
let bare = s.call("plan", json!({ "effects": effects }));
assert!(!bare.is_error, "{}", bare.body);
assert!(bare.body["preview"].get("diffs").is_none(), "{}", bare.body);
assert!(bare.body["preview"].get("summary").is_some());
let with = s.call("plan", json!({ "effects": effects, "diffs": true }));
assert_eq!(
with.body["preview"]["diffs"]["/a.md"]["after"],
"# A\n\nOne.\n\nTwo.\n"
);
assert_eq!(with.body["plan_id"], bare.body["plan_id"]);
let mut p = with.body["preview"].as_object().unwrap().clone();
p.remove("diffs");
assert_eq!(Json::Object(p), bare.body["preview"]);
let off = s.call("plan", json!({ "effects": effects, "diffs": false }));
assert!(off.body["preview"].get("diffs").is_none());
}
#[test]
fn string_list_arguments_take_one_bare_string() {
let mut s = surface();
for (path, content) in [("a.md", "# A\n\nOne.\n"), ("b.md", "# B\n")] {
let out = s.call("observe", json!({ "path": path, "content": content }));
assert!(!out.is_error, "{}", out.body);
}
let one = s.call("docs_read_many", json!({ "docs": "a.md" }));
assert!(!one.is_error, "{}", one.body);
assert_eq!(one.body["items"][0]["path"], "/a.md");
let ids = s.call("blocks_read_many", json!({ "ids": "b_1" }));
assert_eq!(ids.body["nodes"][0]["id"], "b_1");
let roots = s.call("graph", json!({ "roots": "a.md", "degrees": 0 }));
assert_eq!(roots.body["roots"], json!(["d_0"]));
let joined = s.call(
"docs_read_many",
json!({ "docs": "a.md, b.md", "include_ids": true }),
);
assert!(joined.is_error);
assert_eq!(joined.body["error"], "filter_invalid");
assert_eq!(
joined.body["data"]["fix"],
r#"{"docs":["a.md","b.md"],"include_ids":true}"#
);
}
#[test]
fn plain_shape_errors_carry_their_fix() {
let mut s = surface();
let out = s.call(
"observe",
json!({ "path": "a.md", "content": "# A\n\n- one\n - nested\n" }),
);
assert!(!out.is_error, "{}", out.body);
for tool in ["plan", "apply"] {
let e = s.call(tool, json!({ "effects": [{ "op": "insert", "into": { "kind": "block", "id": "b_3" }, "at": "after", "markdown": "x" }] }));
assert!(e.is_error, "{tool}: {}", e.body);
assert_eq!(e.body["error"], "filter_invalid");
assert_eq!(
e.body["data"]["fix"],
r#"{"effects":[{"at":{"after":"b_3"},"into":{"id":"b_2","kind":"block"},"markdown":"x","op":"insert"}]}"#,
"{tool}"
);
}
let e = s.call("plan", json!({ "effects": [{ "op": "set", "target": { "kind": "block", "id": "b_1" }, "field": "text", "value": "x" }] }));
assert_eq!(
e.body["data"]["fix"],
r#"{"effects":[{"field":"markdown","op":"set","target":{"id":"b_1","kind":"block"},"value":"x"}]}"#
);
}
}