use crate::application::plugin_registry::PluginRegistry;
use crate::application::projection_registry::{ProjectionRegistry, TargetForm};
use crate::application::spec_registry::SpecRegistry;
use crate::domain::error::{DomainError, WireError, WireResult};
use crate::domain::graph::Node;
use crate::domain::port::ProjectionInput;
use crate::domain::specification::Specification;
use crate::infrastructure::adapter::Adapter;
use crate::infrastructure::filter::WireFilters;
use crate::infrastructure::storage::SqliteStorage;
use crate::infrastructure::wire_uri::WireUri;
fn current_epoch_secs() -> WireResult<i64> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.map_err(|e| WireError::Other(format!("system clock before unix epoch: {e}")))
}
fn resolve_engine_render(
registry: &PluginRegistry,
hint: Option<&str>,
template: &str,
data: &serde_json::Value,
) -> WireResult<String> {
let id = hint.unwrap_or("handlebars");
let engine = registry
.engine(id)
.ok_or_else(|| WireError::Storage(format!("template engine '{id}' not registered")))?;
engine.render(template, data)
}
fn assert_static_projection_kind(
projection_name: &str,
projection_kind: Option<&str>,
) -> WireResult<()> {
match projection_kind {
None | Some("static") => Ok(()),
Some(other) => Err(WireError::Other(format!(
"projection '{projection_name}' has projection_kind '{other}' — \
non-static kinds require the async path; use wire_prompt_context instead"
))),
}
}
fn build_broadcast_render_data(matched: &[Node], persona_id: Option<&str>) -> serde_json::Value {
let names: Vec<&str> = matched.iter().map(|n| n.name.as_str()).collect();
let nodes_json: Vec<serde_json::Value> = matched
.iter()
.map(|n| {
serde_json::json!({
"id": n.id,
"type": n.r#type,
"metadata": n.metadata,
})
})
.collect();
let mut obj = serde_json::json!({
"count": matched.len(),
"names": names.join(", "),
"nodes": nodes_json,
});
if let Some(pid) = persona_id {
obj.as_object_mut()
.expect("json!({...}) constructs an object")
.insert("persona_id".to_string(), serde_json::json!(pid));
}
obj
}
fn render_named_projection_sync(
proj: &crate::domain::entity::Projection,
data: &serde_json::Value,
registry: &PluginRegistry,
) -> WireResult<RenderedProjection> {
let (engine_hint, kind_hint, _config) = proj.plugin().to_optional_parts();
assert_static_projection_kind(proj.name().as_str(), kind_hint)?;
let rendered = resolve_engine_render(registry, engine_hint, proj.template().as_str(), data)?;
Ok(RenderedProjection {
name: proj.name().as_str().to_owned(),
target_form: proj.target_form(),
rendered,
})
}
#[allow(clippy::too_many_arguments)]
async fn resolve_projection_render_async(
registry: &PluginRegistry,
template_engine_hint: Option<&str>,
projection_kind_hint: Option<&str>,
template: &str,
target_form: TargetForm,
spec_result: &serde_json::Value,
persona_id: Option<&str>,
config: Option<&serde_json::Value>,
) -> WireResult<String> {
let engine_id = template_engine_hint.unwrap_or("handlebars");
if registry.engine(engine_id).is_none() {
return Err(WireError::Storage(format!(
"template engine '{engine_id}' not registered"
)));
}
let kind_id = projection_kind_hint.unwrap_or("static");
let projection = registry
.projection(kind_id)
.ok_or_else(|| WireError::Storage(format!("projection kind '{kind_id}' not registered")))?;
let null = serde_json::Value::Null;
let input = ProjectionInput {
spec_result,
template,
target_form,
persona_id,
config: config.unwrap_or(&null),
};
projection.render(input).await
}
pub struct WireInitInput {
pub persona_id: String,
}
#[derive(Debug)]
pub struct RenderedProjection {
pub name: String,
pub target_form: TargetForm,
pub rendered: String,
}
pub struct WireInitOutput {
pub persona_id: String,
pub projections: Vec<RenderedProjection>,
pub warnings: Vec<String>,
}
pub fn wire_init(
input: WireInitInput,
storage: &SqliteStorage,
registry: &PluginRegistry,
) -> WireResult<WireInitOutput> {
let spec_reg = SpecRegistry::new(storage);
let proj_reg = ProjectionRegistry::new(storage);
let mut projections = Vec::new();
let mut warnings = Vec::new();
for name in proj_reg.list()? {
let Some(proj) = proj_reg.get(&name)? else {
continue;
};
let Some(spec) = spec_reg.get(proj.spec_ref().as_str())? else {
warnings.push(format!(
"projection '{name}': spec_ref '{}' not registered",
proj.spec_ref()
));
continue;
};
let matched = collect_matching_nodes(storage, &spec)?;
let data = build_broadcast_render_data(&matched, Some(input.persona_id.as_str()));
projections.push(render_named_projection_sync(&proj, &data, registry)?);
}
Ok(WireInitOutput {
persona_id: input.persona_id,
projections,
warnings,
})
}
#[derive(Debug)]
pub struct WirePromptContextInput {
pub persona_id: String,
pub projection_names: Option<Vec<String>>,
pub projection_exclude_names: Option<Vec<String>>,
}
#[derive(Debug)]
pub struct WirePromptContextOutput {
pub persona_id: String,
pub prompt_context: String,
pub projections: Vec<RenderedProjection>,
pub warnings: Vec<String>,
}
struct CollectedSlot {
slot: String,
source_uri: String,
target_form: TargetForm,
template: String,
template_engine: Option<String>,
projection_kind: Option<String>,
projection_config: Option<serde_json::Value>,
projection_name: String,
auth: Option<String>,
}
pub async fn wire_prompt_context(
input: WirePromptContextInput,
storage: std::sync::Arc<std::sync::Mutex<SqliteStorage>>,
registry: &PluginRegistry,
) -> WireResult<WirePromptContextOutput> {
let overlays = resolve_persona_overlays(&input.persona_id, registry).await;
let mut warnings = Vec::new();
let collected: Vec<CollectedSlot> = {
let s = storage.lock().map_err(|_| {
crate::domain::error::WireError::Storage("storage mutex poisoned".to_string())
})?;
let proj_reg = ProjectionRegistry::new(&s);
let slots = enumerate_slot_names(
&s,
&input.persona_id,
input.projection_names.as_deref(),
input.projection_exclude_names.as_deref(),
)?;
let mut out: Vec<CollectedSlot> = Vec::new();
for slot in &slots {
if let Some(c) = collect_slot(
slot,
&input.persona_id,
&s,
&proj_reg,
&overlays,
&mut warnings,
)? {
out.push(c);
}
}
out
};
let mut projections = Vec::new();
for c in &collected {
projections.push(
render_collected_slot_async(c, &input.persona_id, registry, &mut warnings).await?,
);
}
let prompt_context = projections
.iter()
.map(|p| p.rendered.as_str())
.collect::<Vec<_>>()
.join("\n");
Ok(WirePromptContextOutput {
persona_id: input.persona_id,
prompt_context,
projections,
warnings,
})
}
async fn resolve_persona_overlays(
persona_id: &str,
registry: &PluginRegistry,
) -> std::collections::BTreeMap<String, crate::application::projection_overlay::ProjectionOverlay> {
use crate::application::projection_overlay::parse_overlay_response;
let overlay_uri = format!("persona-pack://{}/projections", persona_id);
match registry.route(&overlay_uri) {
Ok((adapter, uri)) => match adapter.fetch(&uri).await {
Ok(v) => parse_overlay_response(&v).unwrap_or_default(),
Err(_) => std::collections::BTreeMap::new(),
},
Err(_) => std::collections::BTreeMap::new(),
}
}
fn enumerate_slot_names(
storage: &SqliteStorage,
persona_id: &str,
explicit: Option<&[String]>,
exclude: Option<&[String]>,
) -> WireResult<Vec<String>> {
use crate::application::wiring_mapper;
let base: Vec<String> = if let Some(names) = explicit {
names.to_vec()
} else {
let spec = Specification::And(vec![
Specification::TypeIs(wiring_mapper::WIRING_TYPE.to_string()),
Specification::MetadataEq {
path: wiring_mapper::META_PERSONA.to_string(),
value: serde_json::json!(persona_id),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
nodes
.iter()
.filter_map(|n| wiring_mapper::extract_slot(n).map(str::to_owned))
.collect()
};
if let Some(skip) = exclude {
if !skip.is_empty() {
let skip_set: std::collections::BTreeSet<&str> =
skip.iter().map(String::as_str).collect();
return Ok(base
.into_iter()
.filter(|s| !skip_set.contains(s.as_str()))
.collect());
}
}
Ok(base)
}
fn collect_slot(
slot: &str,
persona_id: &str,
storage: &SqliteStorage,
proj_reg: &ProjectionRegistry,
overlays: &std::collections::BTreeMap<
String,
crate::application::projection_overlay::ProjectionOverlay,
>,
warnings: &mut Vec<String>,
) -> WireResult<Option<CollectedSlot>> {
let node_id = format!("{}.{}", persona_id, slot);
let Some(node) = storage.get_node_by_name(&node_id)? else {
return Ok(None);
};
let Some(source_uri) = crate::application::wiring_mapper::extract_source_uri(&node) else {
warnings.push(format!(
"wiring entry '{node_id}' lacks metadata.source_uri — slot skipped"
));
return Ok(None);
};
let auth = crate::application::wiring_mapper::extract_auth(&node).map(str::to_owned);
let projection_name = match crate::application::wiring_mapper::extract_projection_ref(&node) {
Some(explicit) => explicit.to_owned(),
None => {
crate::application::projection_naming::workflow_emit_projection_name(persona_id, slot)
}
};
let (base_template, base_target, base_engine, base_kind, base_config) =
match proj_reg.get(&projection_name)? {
Some(proj) => {
let (engine, kind, config) = proj.plugin().to_optional_parts();
(
proj.template().as_str().to_owned(),
proj.target_form(),
engine.map(str::to_owned),
kind.map(str::to_owned),
config.cloned(),
)
}
None => {
warnings.push(format!(
"slot '{slot}' has no registered projection \
'{projection_name}' — slot skipped"
));
return Ok(None);
}
};
let (final_template, final_target) = if let Some(o) = overlays.get(slot) {
(o.strategy.merge(&base_template, &o.template), o.target_form)
} else {
(base_template, base_target)
};
Ok(Some(CollectedSlot {
slot: slot.to_string(),
source_uri: source_uri.to_string(),
target_form: final_target,
template: final_template,
template_engine: base_engine,
projection_kind: base_kind,
projection_config: base_config,
projection_name,
auth,
}))
}
async fn render_collected_slot_async(
c: &CollectedSlot,
persona_id: &str,
registry: &PluginRegistry,
warnings: &mut Vec<String>,
) -> WireResult<RenderedProjection> {
let fetch_uri = merge_auth_query(&c.source_uri, c.auth.as_deref());
let fetched = match registry.route(&fetch_uri) {
Ok((adapter, uri)) => match fetch_with_post_filters(&*adapter, &uri, &fetch_uri).await {
Ok(v) => v,
Err(e) => {
warnings.push(format!(
"adapter fetch failed for slot '{}' (uri={}): {e}",
c.slot, c.source_uri
));
serde_json::Value::Null
}
},
Err(e) => {
warnings.push(format!(
"registry route failed for slot '{}' (uri={}): {e}",
c.slot, c.source_uri
));
serde_json::Value::Null
}
};
let fetched_is_null = fetched.is_null();
let entries = vec![serde_json::json!({
"wiring_entry": {
"slot": c.slot,
"source_uri": c.source_uri,
},
"fetched_data": fetched,
})];
let data = serde_json::json!({
"count": 1,
"slot": c.slot,
"entries": entries,
"persona_id": persona_id,
});
let rendered = resolve_projection_render_async(
registry,
c.template_engine.as_deref(),
c.projection_kind.as_deref(),
&c.template,
c.target_form,
&data,
Some(persona_id),
c.projection_config.as_ref(),
)
.await?;
if !fetched_is_null && rendered.trim().is_empty() {
warnings.push(format!(
"slot '{}' rendered empty output despite non-null fetched_data — \
check the template's field paths against `wire_fetch` output \
(projection '{}')",
c.slot, c.projection_name
));
}
Ok(RenderedProjection {
name: c.projection_name.clone(),
target_form: c.target_form,
rendered,
})
}
async fn fetch_with_post_filters(
adapter: &dyn Adapter,
uri: &WireUri,
raw_fetch_uri: &str,
) -> WireResult<serde_json::Value> {
let plan = if adapter.post_filterable() {
WireFilters::split_post(uri, adapter.filter_caps())?
} else {
None
};
match plan {
None => adapter.fetch(uri).await,
Some(plan) => {
let stripped = strip_query_params(raw_fetch_uri, &plan.strip_keys);
let stripped_uri = WireUri::parse(&stripped)?;
let mut fetched = adapter.fetch(&stripped_uri).await?;
plan.apply(&mut fetched)?;
Ok(fetched)
}
}
}
fn strip_query_params(raw_uri: &str, keys: &[&str]) -> String {
let (base, fragment) = match raw_uri.split_once('#') {
Some((b, f)) => (b, Some(f)),
None => (raw_uri, None),
};
let (path, query) = match base.split_once('?') {
Some((p, q)) => (p, q),
None => return raw_uri.to_string(),
};
let kept: Vec<&str> = query
.split('&')
.filter(|pair| {
let key = pair.split_once('=').map(|(k, _)| k).unwrap_or(pair);
!keys.contains(&key)
})
.collect();
let rebuilt = if kept.is_empty() {
path.to_string()
} else {
format!("{path}?{}", kept.join("&"))
};
match fragment {
Some(f) => format!("{rebuilt}#{f}"),
None => rebuilt,
}
}
fn merge_auth_query(source_uri: &str, meta_auth: Option<&str>) -> String {
let Some(key) = meta_auth else {
return source_uri.to_string();
};
match WireUri::parse(source_uri) {
Ok(parsed) if parsed.query_get("auth").is_none() => {
append_query_param(source_uri, "auth", key)
}
_ => source_uri.to_string(),
}
}
fn append_query_param(raw_uri: &str, key: &str, value: &str) -> String {
let (base, fragment) = match raw_uri.split_once('#') {
Some((b, f)) => (b, Some(f)),
None => (raw_uri, None),
};
let sep = if base.contains('?') { '&' } else { '?' };
let merged = format!("{base}{sep}{key}={value}");
match fragment {
Some(f) => format!("{merged}#{f}"),
None => merged,
}
}
#[derive(Debug)]
pub struct WireSlotRegisterInput {
pub persona_id: String,
pub slot: String,
pub source_uri: String,
pub template: String,
pub target_form: TargetForm,
pub maintenance_exempt: Option<bool>,
pub auth: Option<String>,
}
#[derive(Debug)]
pub struct WireSlotRegisterOutput {
pub node_name: String,
pub node_id: String,
pub node_created: bool,
pub spec_name: String,
pub projection_name: String,
}
pub fn wire_slot_register(
input: WireSlotRegisterInput,
storage: &SqliteStorage,
) -> WireResult<WireSlotRegisterOutput> {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let persona = PersonaId::new(input.persona_id.clone())?;
let slot = Slot::new(input.slot.clone())?;
let source = Source::new(input.source_uri.clone())?;
let node_name = format!("{}.{}", persona.as_str(), slot.as_str());
let spec_name = format!("{}.spec.{}", persona.as_str(), slot.as_str());
let projection_name = crate::application::projection_naming::workflow_emit_projection_name(
persona.as_str(),
slot.as_str(),
);
let mut extras = serde_json::Map::new();
if let Some(flag) = input.maintenance_exempt {
extras.insert(
wiring_mapper::META_MAINTENANCE_EXEMPT.to_string(),
serde_json::Value::Bool(flag),
);
}
if let Some(auth) = &input.auth {
extras.insert(
wiring_mapper::META_AUTH.to_string(),
serde_json::Value::String(auth.clone()),
);
}
let canonical = wiring_mapper::wiring_metadata_object(&persona, &slot, &source, Some(extras));
let (node_id, node_created) = match storage.get_node_by_name(&node_name)? {
Some(existing) => {
let mut base = match existing.metadata {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
if let serde_json::Value::Object(patch) = &canonical {
for (k, v) in patch {
base.insert(k.clone(), v.clone());
}
}
let updated =
storage.update_node_metadata(&existing.id, &serde_json::Value::Object(base))?;
if !updated {
return Err(WireError::Storage(format!(
"wire_slot_register: node '{node_name}' vanished between read and write"
)));
}
(existing.id, false)
}
None => {
let node = Node {
id: crate::domain::graph::Ulid::new(),
name: node_name.clone(),
r#type: wiring_mapper::WIRING_TYPE.to_string(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: canonical,
};
storage.insert_node(&node)?;
(node.id, true)
}
};
let spec = Specification::And(vec![
Specification::TypeIs(wiring_mapper::WIRING_TYPE.to_string()),
Specification::MetadataEq {
path: wiring_mapper::META_PERSONA.to_string(),
value: serde_json::json!(persona.as_str()),
},
Specification::MetadataEq {
path: wiring_mapper::META_SLOT.to_string(),
value: serde_json::json!(slot.as_str()),
},
]);
SpecRegistry::new(storage).register(&spec_name, &spec)?;
let projection = crate::domain::entity::projection::Projection::from_parts(
projection_name.clone(),
spec_name.clone(),
input.template,
input.target_form,
crate::domain::entity::projection::PluginDispatch::Default,
)?;
ProjectionRegistry::new(storage).register(&projection)?;
Ok(WireSlotRegisterOutput {
node_name,
node_id: node_id.to_string(),
node_created,
spec_name,
projection_name,
})
}
#[derive(Debug)]
pub struct WireSlotDeleteInput {
pub persona_id: String,
pub slot: String,
}
#[derive(Debug)]
pub struct WireSlotDeleteOutput {
pub node_name: String,
pub node_deleted: bool,
pub spec_name: String,
pub spec_deleted: bool,
pub projection_name: String,
pub projection_deleted: bool,
}
pub fn wire_slot_delete(
input: WireSlotDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireSlotDeleteOutput> {
let node_name = format!("{}.{}", input.persona_id, input.slot);
let spec_name = format!("{}.spec.{}", input.persona_id, input.slot);
let projection_name = crate::application::projection_naming::workflow_emit_projection_name(
&input.persona_id,
&input.slot,
);
let node_deleted = match storage.lookup_node_id_by_name(&node_name)? {
Some(id) => storage.delete_node(&id)?,
None => false,
};
let spec_deleted = match storage.resolve_specification_id_or_name(&spec_name)? {
Some(id) => storage.delete_specification(&id)?,
None => false,
};
let projection_deleted = match storage.resolve_projection_id_or_name(&projection_name)? {
Some(id) => storage.delete_projection(&id)?,
None => false,
};
Ok(WireSlotDeleteOutput {
node_name,
node_deleted,
spec_name,
spec_deleted,
projection_name,
projection_deleted,
})
}
#[derive(Debug)]
pub struct WireFetchInput {
pub source_uri: Option<String>,
pub persona_id: Option<String>,
pub slot: Option<String>,
}
#[derive(Debug)]
pub struct WireFetchOutput {
pub source_uri: String,
pub fetched_data: serde_json::Value,
}
pub async fn wire_fetch(
input: WireFetchInput,
storage: std::sync::Arc<std::sync::Mutex<SqliteStorage>>,
registry: &PluginRegistry,
) -> WireResult<WireFetchOutput> {
let (stored_uri, fetch_uri) = match (&input.source_uri, &input.persona_id, &input.slot) {
(Some(uri), None, None) => (uri.clone(), uri.clone()),
(None, Some(persona), Some(slot)) => {
let node_name = format!("{persona}.{slot}");
let node = {
let s = storage
.lock()
.map_err(|_| WireError::Storage("storage mutex poisoned".to_string()))?;
s.get_node_by_name(&node_name)?.ok_or_else(|| {
WireError::Domain(DomainError::NotFound(format!("wiring entry: {node_name}")))
})?
};
let source_uri = crate::application::wiring_mapper::extract_source_uri(&node)
.ok_or_else(|| {
WireError::Domain(DomainError::InvalidMetadata(format!(
"wiring entry '{node_name}' lacks metadata.source_uri"
)))
})?
.to_owned();
let auth = crate::application::wiring_mapper::extract_auth(&node);
let merged = merge_auth_query(&source_uri, auth);
(source_uri, merged)
}
_ => {
return Err(WireError::Other(
"wire_fetch: supply either `source_uri` alone, or `persona_id` + `slot`"
.to_string(),
))
}
};
let (adapter, uri) = registry.route(&fetch_uri)?;
let fetched_data = fetch_with_post_filters(&*adapter, &uri, &fetch_uri).await?;
Ok(WireFetchOutput {
source_uri: stored_uri,
fetched_data,
})
}
#[derive(Debug)]
pub struct WireMaterializeInput {
pub persona_id: String,
pub slot: String,
pub item_path: Option<String>,
pub item_id_key: Option<String>,
}
#[derive(Debug)]
pub struct WireMaterializeOutput {
pub tank_uri: String,
pub snapshot_id: String,
pub item_count: usize,
pub new_item_count: usize,
pub deduped_count: usize,
pub registry_node_id: String,
pub registry_created: bool,
}
pub async fn wire_materialize(
input: WireMaterializeInput,
storage: std::sync::Arc<std::sync::Mutex<SqliteStorage>>,
registry: &PluginRegistry,
) -> WireResult<WireMaterializeOutput> {
use crate::application::wiring_mapper;
use crate::domain::graph::{Edge, Ulid};
use crate::infrastructure::storage::{TankItemRecord, TankSnapshotRecord};
let persona = &input.persona_id;
let slot = &input.slot;
let node_name = format!("{persona}.{slot}");
let registry_name = format!("{persona}.tank.{slot}");
let tank_key = format!("{persona}/{slot}");
let tank_uri = format!("tank://{persona}/{slot}");
let (source_uri, fetch_uri, wiring_node_id, reg_item_path, reg_item_id_key) = {
let s = storage
.lock()
.map_err(|_| WireError::Storage("storage mutex poisoned".to_string()))?;
let node = s.get_node_by_name(&node_name)?.ok_or_else(|| {
WireError::Domain(DomainError::NotFound(format!("wiring entry: {node_name}")))
})?;
let source_uri = wiring_mapper::extract_source_uri(&node)
.ok_or_else(|| {
WireError::Domain(DomainError::InvalidMetadata(format!(
"wiring entry '{node_name}' lacks metadata.source_uri"
)))
})?
.to_owned();
let auth = wiring_mapper::extract_auth(&node).map(str::to_owned);
let fetch_uri = merge_auth_query(&source_uri, auth.as_deref());
let existing_reg = s.get_node_by_name(®istry_name)?;
let reg_item_path = existing_reg
.as_ref()
.and_then(|n| n.metadata.get("item_path").and_then(|v| v.as_str()))
.map(str::to_owned);
let reg_item_id_key = existing_reg
.as_ref()
.and_then(|n| n.metadata.get("item_id_key").and_then(|v| v.as_str()))
.map(str::to_owned);
(
source_uri,
fetch_uri,
node.id,
reg_item_path,
reg_item_id_key,
)
};
if WireUri::parse(&source_uri)
.map(|u| u.scheme() == "tank")
.unwrap_or(false)
{
return Err(WireError::Storage(
"wire_materialize: cannot materialize a tank:// source into itself".to_string(),
));
}
let item_path = input.item_path.clone().or(reg_item_path);
let item_id_key = input.item_id_key.clone().or(reg_item_id_key);
let (adapter, uri) = registry.route(&fetch_uri)?;
let fetched = fetch_with_post_filters(&*adapter, &uri, &fetch_uri).await?;
let items: Vec<serde_json::Value> = match &item_path {
Some(path) => {
let arr_node = fetched.pointer(path).ok_or_else(|| {
WireError::Storage(format!(
"wire_materialize: item_path '{path}' not found in fetched data"
))
})?;
let arr = arr_node.as_array().ok_or_else(|| {
WireError::Storage(format!(
"wire_materialize: item_path '{path}' is not an array"
))
})?;
arr.clone()
}
None => vec![fetched.clone()],
};
let now = current_epoch_secs()?;
let content_hash = tank_content_hash(&fetched)?;
let mut id_gen = ulid::Generator::new();
let mut item_records: Vec<TankItemRecord> = Vec::with_capacity(items.len());
for item in &items {
let identity = tank_item_identity(item, item_id_key.as_deref())?;
let id = id_gen
.generate()
.map_err(|e| WireError::Storage(format!("wire_materialize: ulid generation: {e}")))?;
item_records.push(TankItemRecord {
id,
identity,
observed_at: now,
payload: item.clone(),
mime_type: "application/json".to_string(),
});
}
let filters_applied = match WireUri::parse(&source_uri) {
Ok(u) => serde_json::Value::Object(
u.query()
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect(),
),
Err(_) => serde_json::json!({}),
};
let (registry_node_id, registry_created, snapshot_id, item_count, new_item_count) = {
let s = storage
.lock()
.map_err(|_| WireError::Storage("storage mutex poisoned".to_string()))?;
let mut reg_meta = serde_json::Map::new();
reg_meta.insert("source_uri".to_string(), serde_json::json!(tank_uri));
reg_meta.insert("upstream".to_string(), serde_json::json!(source_uri));
if let Some(p) = &item_path {
reg_meta.insert("item_path".to_string(), serde_json::json!(p));
}
if let Some(k) = &item_id_key {
reg_meta.insert("item_id_key".to_string(), serde_json::json!(k));
}
reg_meta.insert(
"prov".to_string(),
serde_json::json!({ "wasDerivedFrom": source_uri }),
);
let (registry_node_id, registry_created) = match s.get_node_by_name(®istry_name)? {
Some(existing) => {
let mut base = match existing.metadata {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
for (k, v) in ®_meta {
base.insert(k.clone(), v.clone());
}
let updated =
s.update_node_metadata(&existing.id, &serde_json::Value::Object(base))?;
if !updated {
return Err(WireError::Storage(format!(
"wire_materialize: registry node '{registry_name}' vanished \
between read and write"
)));
}
(existing.id, false)
}
None => {
let node = Node {
id: Ulid::new(),
name: registry_name.clone(),
r#type: "snapshot_registry".to_string(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: serde_json::Value::Object(reg_meta.clone()),
};
s.insert_node(&node)?;
(node.id, true)
}
};
let already_linked = s
.list_edges_from(®istry_node_id)?
.iter()
.any(|e| e.tgt_node == wiring_node_id && e.kind == "archives");
if !already_linked {
let edge = Edge {
id: Ulid::new(),
name: None,
src_node: registry_node_id,
tgt_node: wiring_node_id,
kind: "archives".to_string(),
severity: None,
metadata: serde_json::json!({ "prov": "wasDerivedFrom" }),
version: 1,
prev_id: None,
};
s.insert_edge(&edge)?;
}
let snapshot_id = Ulid::new();
let item_count = item_records.len();
s.tank_insert_snapshot(&TankSnapshotRecord {
id: snapshot_id,
tank_key: tank_key.clone(),
source_uri: source_uri.clone(),
fetched_at: now,
filters_applied,
content_hash,
item_count,
new_item_count: 0,
})?;
let new_item_count = s.tank_append_items(&tank_key, &snapshot_id, &item_records)?;
s.tank_update_snapshot_new_count(&snapshot_id, new_item_count)?;
(
registry_node_id,
registry_created,
snapshot_id,
item_count,
new_item_count,
)
};
Ok(WireMaterializeOutput {
tank_uri,
snapshot_id: snapshot_id.to_string(),
item_count,
new_item_count,
deduped_count: item_count - new_item_count,
registry_node_id: registry_node_id.to_string(),
registry_created,
})
}
fn tank_content_hash(v: &serde_json::Value) -> WireResult<String> {
use sha2::{Digest, Sha256};
let s = serde_json::to_string(v).map_err(|e| WireError::Storage(e.to_string()))?;
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
fn tank_item_identity(item: &serde_json::Value, item_id_key: Option<&str>) -> WireResult<String> {
if let Some(key) = item_id_key {
match item.get(key) {
Some(serde_json::Value::String(s)) => return Ok(s.clone()),
Some(serde_json::Value::Number(n)) => return Ok(n.to_string()),
_ => {} }
}
tank_content_hash(item)
}
fn collect_matching_nodes(storage: &SqliteStorage, spec: &Specification) -> WireResult<Vec<Node>> {
let mut out = Vec::new();
for t in storage.list_types_by_kind("node")? {
for n in storage.list_nodes_by_type(&t)? {
if spec.is_satisfied_by(&n) {
out.push(n);
}
}
}
Ok(out)
}
pub struct GraphScanSummary {
pub orphan_node_count: usize,
pub total_node_count: usize,
pub total_edge_count: usize,
}
pub(crate) fn is_self_attached_wiring(node: &crate::domain::graph::Node) -> bool {
use crate::application::wiring_mapper;
if !node.metadata.is_object() {
return false;
}
let has_source_uri = wiring_mapper::extract_source_uri(node)
.map(|s| !s.is_empty())
.unwrap_or(false);
let is_exempt = wiring_mapper::extract_maintenance_exempt(node);
has_source_uri || is_exempt
}
pub fn graph_scan_summary(storage: &SqliteStorage) -> WireResult<GraphScanSummary> {
use crate::application::workflow_mapper::WORKFLOW_TYPE;
let mut total_nodes = 0_usize;
let mut total_edges = 0_usize;
let mut orphan = 0_usize;
for t in storage.list_types_by_kind("node")? {
if t == WORKFLOW_TYPE {
continue;
}
for n in storage.list_nodes_by_type(&t)? {
total_nodes += 1;
let out_edges = storage.list_edges_from(&n.id)?;
let in_edges = storage.list_edges_to(&n.id)?;
total_edges += out_edges.len();
if out_edges.is_empty() && in_edges.is_empty() && !is_self_attached_wiring(&n) {
orphan += 1;
}
}
}
Ok(GraphScanSummary {
orphan_node_count: orphan,
total_node_count: total_nodes,
total_edge_count: total_edges,
})
}
pub struct WireCloseInput {
pub persona_id: String,
}
pub struct WireCloseOutput {
pub persona_id: String,
pub orphan_node_count: usize,
pub total_node_count: usize,
pub total_edge_count: usize,
pub report_markdown: String,
}
pub fn wire_close(input: WireCloseInput, storage: &SqliteStorage) -> WireResult<WireCloseOutput> {
let summary = graph_scan_summary(storage)?;
let persona = &input.persona_id;
let report_markdown = format!(
"# wire_close report for `{persona}`\n\n\
- total nodes: {total_nodes}\n\
- total edges: {total_edges}\n\
- orphan nodes (no edges, not self-attached): {orphan}\n",
total_nodes = summary.total_node_count,
total_edges = summary.total_edge_count,
orphan = summary.orphan_node_count,
);
Ok(WireCloseOutput {
persona_id: input.persona_id,
orphan_node_count: summary.orphan_node_count,
total_node_count: summary.total_node_count,
total_edge_count: summary.total_edge_count,
report_markdown,
})
}
pub struct WireDoctorOutput {
pub report_markdown: String,
}
pub fn wire_doctor(
storage: &SqliteStorage,
persona_id: Option<String>,
registry: &PluginRegistry,
) -> WireResult<WireDoctorOutput> {
let report_markdown = crate::application::doctor::run(storage, persona_id, registry)?;
Ok(WireDoctorOutput { report_markdown })
}
#[derive(Debug)]
pub struct WireQueryInput {
pub spec: Option<Specification>,
pub spec_ref: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug)]
pub struct WireQueryNode {
pub id: String,
pub name: String,
pub r#type: String,
pub metadata: serde_json::Value,
}
#[derive(Debug)]
pub struct WireQueryOutput {
pub matched: Vec<WireQueryNode>,
pub total_count: usize,
pub returned_count: usize,
}
pub fn wire_query(input: WireQueryInput, storage: &SqliteStorage) -> WireResult<WireQueryOutput> {
let resolved: Specification = match (input.spec, input.spec_ref.as_deref()) {
(Some(s), None) => s,
(None, Some(id_or_name)) => {
let name = match storage.resolve_specification_id_or_name(id_or_name)? {
Some(id) => storage.get_specification_name_by_id(&id)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec: {id_or_name} (resolved id {id} has no row)"
)))
})?,
None => {
return Err(crate::domain::error::WireError::Domain(
DomainError::NotFound(format!("spec: {id_or_name}")),
));
}
};
SpecRegistry::new(storage).get(&name)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec: {name}"
)))
})?
}
(Some(_), Some(_)) => {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("spec and spec_ref are mutually exclusive".into()),
));
}
(None, None) => {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("either spec or spec_ref is required".into()),
));
}
};
let all = collect_matching_nodes(storage, &resolved)?;
let total_count = all.len();
let offset = input.offset.unwrap_or(0);
let slice: Vec<Node> = match input.limit {
Some(lim) => all.into_iter().skip(offset).take(lim).collect(),
None => all.into_iter().skip(offset).collect(),
};
let returned_count = slice.len();
let matched = slice
.into_iter()
.map(|n| WireQueryNode {
id: n.id.to_string(),
name: n.name,
r#type: n.r#type,
metadata: n.metadata,
})
.collect();
Ok(WireQueryOutput {
matched,
total_count,
returned_count,
})
}
#[derive(Debug)]
pub struct WireRenderInput {
pub projection_ref: String,
}
#[derive(Debug)]
pub struct WireRenderOutput {
pub name: String,
pub target_form: TargetForm,
pub rendered: String,
}
pub fn wire_render(
input: WireRenderInput,
storage: &SqliteStorage,
registry: &PluginRegistry,
) -> WireResult<WireRenderOutput> {
let projection_name = match storage.resolve_projection_id_or_name(&input.projection_ref)? {
Some(id) => storage.get_projection_name_by_id(&id)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"projection: {} (resolved id {} has no row)",
input.projection_ref, id
)))
})?,
None => {
return Err(crate::domain::error::WireError::Domain(
DomainError::NotFound(format!("projection: {}", input.projection_ref)),
));
}
};
let proj = ProjectionRegistry::new(storage)
.get(&projection_name)?
.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"projection: {}",
input.projection_ref
)))
})?;
let spec = SpecRegistry::new(storage)
.get(proj.spec_ref().as_str())?
.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec_ref (dangling): {}",
proj.spec_ref()
)))
})?;
let matched = collect_matching_nodes(storage, &spec)?;
let data = build_broadcast_render_data(&matched, None);
let r = render_named_projection_sync(&proj, &data, registry)?;
Ok(WireRenderOutput {
name: r.name,
target_form: r.target_form,
rendered: r.rendered,
})
}
#[derive(Debug)]
pub struct WireContextGetInput {
pub persona_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WiringSummary {
pub slot: String,
pub source_uri: String,
pub projection_ref: Option<String>,
pub maintenance_exempt: bool,
}
#[derive(Debug)]
pub struct WireContextGetOutput {
pub persona_id: String,
pub wirings: Vec<WiringSummary>,
pub workflows: Vec<WorkflowSummary>,
}
pub fn wire_context_get(
input: WireContextGetInput,
storage: &SqliteStorage,
) -> WireResult<WireContextGetOutput> {
use crate::application::wiring_mapper;
use crate::application::workflow_mapper::WORKFLOW_TYPE;
use crate::domain::entity::context_wiring::ContextWiring;
use crate::domain::entity::persona_id::PersonaId;
let persona = PersonaId::new(input.persona_id.clone())?;
let context = ContextWiring::new(persona.clone());
let wirings = list_persona_wirings(&context, storage)?;
let workflows = list_persona_workflow_summaries(&context, storage)?;
let mut wirings = wirings;
wirings.sort_by(|a, b| a.slot.cmp(&b.slot));
let mut workflows = workflows;
workflows.sort_by(|a, b| a.id.cmp(&b.id));
let _ = (wiring_mapper::WIRING_TYPE, WORKFLOW_TYPE);
Ok(WireContextGetOutput {
persona_id: context.persona_id().as_str().to_owned(),
wirings,
workflows,
})
}
fn list_persona_wirings(
context: &crate::domain::entity::context_wiring::ContextWiring,
storage: &SqliteStorage,
) -> WireResult<Vec<WiringSummary>> {
use crate::application::projection_naming::workflow_emit_projection_name;
use crate::application::wiring_mapper::{self, WIRING_TYPE};
use crate::domain::specification::Specification;
let spec = Specification::And(vec![
Specification::TypeIs(WIRING_TYPE.to_string()),
Specification::MetadataEq {
path: wiring_mapper::META_PERSONA.to_string(),
value: serde_json::Value::String(context.persona_id().as_str().to_owned()),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
let registry = ProjectionRegistry::new(storage);
let mut out = Vec::with_capacity(nodes.len());
for node in &nodes {
let Some(slot) = wiring_mapper::extract_slot(node) else {
continue;
};
let Some(source_uri) = wiring_mapper::extract_source_uri(node) else {
continue;
};
let projection_ref = match wiring_mapper::extract_projection_ref(node) {
Some(explicit) => Some(explicit.to_owned()),
None => {
let derived = workflow_emit_projection_name(context.persona_id().as_str(), slot);
if registry.get(&derived)?.is_some() {
Some(derived)
} else {
None
}
}
};
out.push(WiringSummary {
slot: slot.to_owned(),
source_uri: source_uri.to_owned(),
projection_ref,
maintenance_exempt: wiring_mapper::extract_maintenance_exempt(node),
});
}
Ok(out)
}
fn list_persona_workflow_summaries(
context: &crate::domain::entity::context_wiring::ContextWiring,
storage: &SqliteStorage,
) -> WireResult<Vec<WorkflowSummary>> {
use crate::application::workflow_mapper::{self, WORKFLOW_TYPE};
use crate::domain::specification::Specification;
let spec = Specification::And(vec![
Specification::TypeIs(WORKFLOW_TYPE.to_string()),
Specification::MetadataEq {
path: workflow_mapper::META_PERSONA.to_string(),
value: serde_json::Value::String(context.persona_id().as_str().to_owned()),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
let summaries = nodes
.into_iter()
.filter_map(|n| node_to_summary(n).ok())
.collect();
Ok(summaries)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireNodeUpdateMode {
Merge,
Replace,
}
impl WireNodeUpdateMode {
pub fn as_str(self) -> &'static str {
match self {
WireNodeUpdateMode::Merge => "merge",
WireNodeUpdateMode::Replace => "replace",
}
}
pub fn parse(s: &str) -> WireResult<Self> {
match s {
"merge" => Ok(WireNodeUpdateMode::Merge),
"replace" => Ok(WireNodeUpdateMode::Replace),
other => Err(WireError::Other(format!(
"unknown wire_node_update mode '{other}' — expected 'merge' or 'replace'"
))),
}
}
}
#[derive(Debug)]
pub struct WireNodeUpdateInput {
pub id: String,
pub metadata_patch: serde_json::Value,
pub mode: WireNodeUpdateMode,
}
#[derive(Debug)]
pub struct WireNodeUpdateOutput {
pub id: String,
pub mode: WireNodeUpdateMode,
pub metadata: serde_json::Value,
}
pub fn wire_node_update(
input: WireNodeUpdateInput,
storage: &SqliteStorage,
) -> WireResult<WireNodeUpdateOutput> {
if !input.metadata_patch.is_object() {
return Err(WireError::Other(format!(
"wire_node_update: metadata_patch must be a JSON object, got {}",
type_name_of(&input.metadata_patch)
)));
}
let resolved = storage
.resolve_node_id_or_name(&input.id)?
.ok_or_else(|| WireError::Domain(DomainError::NotFound(format!("node: {}", input.id))))?;
let Some(existing) = storage.get_node(&resolved)? else {
return Err(WireError::Domain(DomainError::NotFound(format!(
"node: {}",
input.id
))));
};
let final_metadata = match input.mode {
WireNodeUpdateMode::Replace => input.metadata_patch.clone(),
WireNodeUpdateMode::Merge => {
let mut base = match existing.metadata {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
if let serde_json::Value::Object(patch_obj) = &input.metadata_patch {
for (k, v) in patch_obj {
if v.is_null() {
base.remove(k);
} else {
base.insert(k.clone(), v.clone());
}
}
}
serde_json::Value::Object(base)
}
};
let updated = storage.update_node_metadata(&resolved, &final_metadata)?;
if !updated {
return Err(WireError::Storage(format!(
"wire_node_update: row '{}' vanished between read and write",
input.id
)));
}
Ok(WireNodeUpdateOutput {
id: input.id,
mode: input.mode,
metadata: final_metadata,
})
}
fn type_name_of(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
#[derive(Debug)]
pub struct WireDeleteInput {
pub id_or_name: String,
}
#[derive(Debug)]
pub struct WireDeleteOutput {
pub kind: &'static str,
pub id_or_name: String,
pub deleted: bool,
}
pub fn wire_node_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_node_id_or_name(&input.id_or_name)? {
None => false,
Some(id) => storage.delete_node(&id)?,
};
Ok(WireDeleteOutput {
kind: "node",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_edge_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_edge_id_or_name(&input.id_or_name)? {
None => false,
Some(id) => storage.delete_edge(&id)?,
};
Ok(WireDeleteOutput {
kind: "edge",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_spec_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_specification_id_or_name(&input.id_or_name)? {
Some(id) => storage.delete_specification(&id)?,
None => false,
};
Ok(WireDeleteOutput {
kind: "spec",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_projection_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_projection_id_or_name(&input.id_or_name)? {
Some(id) => storage.delete_projection(&id)?,
None => false,
};
Ok(WireDeleteOutput {
kind: "projection",
id_or_name: input.id_or_name,
deleted,
})
}
pub struct WireNodesCreateBatchInput {
pub nodes: Vec<Node>,
}
pub struct WireBatchOutput {
pub inserted_count: usize,
pub failed_at: Option<usize>,
pub error_message: Option<String>,
}
pub fn wire_nodes_create_batch(
input: WireNodesCreateBatchInput,
storage: &SqliteStorage,
) -> WireResult<WireBatchOutput> {
for (i, n) in input.nodes.iter().enumerate() {
if let Err(e) = storage.insert_node(n) {
return Ok(WireBatchOutput {
inserted_count: i,
failed_at: Some(i),
error_message: Some(e.to_string()),
});
}
}
Ok(WireBatchOutput {
inserted_count: input.nodes.len(),
failed_at: None,
error_message: None,
})
}
pub struct WireEdgesCreateBatchInput {
pub edges: Vec<crate::domain::graph::Edge>,
}
pub fn wire_edges_create_batch(
input: WireEdgesCreateBatchInput,
storage: &SqliteStorage,
) -> WireResult<WireBatchOutput> {
for (i, e) in input.edges.iter().enumerate() {
if let Err(err) = storage.insert_edge(e) {
return Ok(WireBatchOutput {
inserted_count: i,
failed_at: Some(i),
error_message: Some(err.to_string()),
});
}
}
Ok(WireBatchOutput {
inserted_count: input.edges.len(),
failed_at: None,
error_message: None,
})
}
use crate::application::workflow_mapper::{
node_to_workflow, parse_action, parse_trigger, workflow_to_node, WORKFLOW_TYPE,
};
use crate::domain::entity::workflow::{Action, Trigger, Workflow, WorkflowId};
use crate::domain::entity::PersonaId;
#[derive(Debug)]
pub struct WireWorkflowRegisterInput {
pub id: String,
pub persona_id: Option<String>,
pub trigger: serde_json::Value,
pub action: serde_json::Value,
pub enabled: Option<bool>,
}
#[derive(Debug)]
pub struct WireWorkflowRegisterOutput {
pub id: String,
}
pub fn wire_workflow_register(
input: WireWorkflowRegisterInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowRegisterOutput> {
let workflow = build_workflow_from_register_input(input)?;
let node = workflow_to_node(&workflow);
storage.insert_node(&node)?;
Ok(WireWorkflowRegisterOutput {
id: workflow.id().as_str().to_owned(),
})
}
fn build_workflow_from_register_input(input: WireWorkflowRegisterInput) -> WireResult<Workflow> {
let id = WorkflowId::new(input.id)?;
let persona_id = match input.persona_id {
Some(p) => Some(PersonaId::new(p)?),
None => None,
};
let trigger = parse_trigger(&input.trigger)?;
let action = parse_action(&input.action)?;
Ok(Workflow::new(
id,
persona_id,
trigger,
action,
input.enabled.unwrap_or(true),
))
}
#[derive(Debug)]
pub struct WireWorkflowListInput {
pub persona_id: Option<String>,
pub trigger_kind: Option<String>,
pub enabled_only: Option<bool>,
}
#[derive(Debug)]
pub struct WorkflowSummary {
pub id: String,
pub persona_id: Option<String>,
pub trigger: serde_json::Value,
pub action: serde_json::Value,
pub enabled: bool,
}
#[derive(Debug)]
pub struct WireWorkflowListOutput {
pub workflows: Vec<WorkflowSummary>,
}
pub fn wire_workflow_list(
input: WireWorkflowListInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowListOutput> {
let spec = Specification::TypeIs(WORKFLOW_TYPE.to_string());
let nodes = collect_matching_nodes(storage, &spec)?;
let enabled_only = input.enabled_only.unwrap_or(true);
let workflows = nodes
.into_iter()
.filter_map(|n| node_to_summary(n).ok())
.filter(|w| {
if enabled_only && !w.enabled {
return false;
}
if let Some(p) = input.persona_id.as_ref() {
if w.persona_id.as_deref() != Some(p.as_str()) {
return false;
}
}
if let Some(tk) = input.trigger_kind.as_ref() {
if w.trigger.get("kind").and_then(|v| v.as_str()) != Some(tk.as_str()) {
return false;
}
}
true
})
.collect();
Ok(WireWorkflowListOutput { workflows })
}
fn node_to_summary(node: Node) -> WireResult<WorkflowSummary> {
use crate::application::workflow_mapper;
let persona_id = workflow_mapper::extract_persona(&node).map(str::to_owned);
let trigger = workflow_mapper::extract_trigger_value(&node);
let action = workflow_mapper::extract_action_value(&node);
let enabled = workflow_mapper::extract_enabled(&node);
Ok(WorkflowSummary {
id: node.name,
persona_id,
trigger,
action,
enabled,
})
}
#[derive(Debug)]
pub struct WireWorkflowFireInput {
pub id: Option<String>,
pub event: Option<String>,
pub persona_id: Option<String>,
pub dry_run: Option<bool>,
}
#[derive(Debug)]
pub struct ResolvedFire {
pub id: String,
pub persona_id: Option<String>,
pub action_kind: String,
pub action_emit_projection_names: Option<Vec<String>>,
pub dry_run: bool,
}
#[derive(Debug)]
pub struct WireWorkflowFireOutput {
pub fired: Vec<ResolvedFire>,
pub skipped: Vec<(String, String)>, }
pub fn wire_workflow_fire(
input: WireWorkflowFireInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowFireOutput> {
if input.id.is_some() == input.event.is_some() {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("exactly one of `id` or `event` is required".to_string()),
));
}
let dry_run = input.dry_run.unwrap_or(false);
let candidates: Vec<Workflow> = if let Some(id) = input.id.as_ref() {
let resolved = storage.resolve_node_id_or_name(id)?;
let Some(node_id) = resolved else {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(id.clone(), "workflow not found".to_string())],
});
};
let Some(node) = storage.get_node(&node_id)? else {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(id.clone(), "workflow not found".to_string())],
});
};
if node.r#type != WORKFLOW_TYPE {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(
id.clone(),
format!("node type is '{}', expected '{WORKFLOW_TYPE}'", node.r#type),
)],
});
}
vec![node_to_workflow(&node)?]
} else {
let spec = Specification::TypeIs(WORKFLOW_TYPE.to_string());
collect_matching_nodes(storage, &spec)?
.iter()
.filter_map(|n| node_to_workflow(n).ok())
.collect()
};
let mut fired = Vec::new();
let mut skipped = Vec::new();
let event = input.event.as_deref();
for w in candidates {
let id_str = w.id().as_str().to_owned();
if !w.enabled() {
skipped.push((id_str, "enabled=false".to_string()));
continue;
}
if let Some(persona_filter) = input.persona_id.as_ref() {
if w.persona_id().map(|p| p.as_str()) != Some(persona_filter.as_str()) {
skipped.push((
id_str,
format!("persona scope mismatch (want={persona_filter})"),
));
continue;
}
}
if let Some(ev) = event {
match w.trigger() {
Trigger::OnEvent { event: wf_event } => {
if wf_event != ev {
skipped.push((id_str, format!("trigger.event='{wf_event}' != '{ev}'")));
continue;
}
}
Trigger::OnDemand => {
skipped.push((
id_str,
"trigger.kind='on_demand' does not match event fan-out".to_string(),
));
continue;
}
}
}
let (action_kind, action_emit_projection_names) = match w.action() {
Action::NoOp => ("no_op".to_string(), None),
Action::EmitProjection { slots } => (
"emit_projection".to_string(),
Some(slots.iter().map(|s| s.as_str().to_owned()).collect()),
),
};
fired.push(ResolvedFire {
id: w.id().as_str().to_owned(),
persona_id: w.persona_id().map(|p| p.as_str().to_owned()),
action_kind,
action_emit_projection_names,
dry_run,
});
}
Ok(WireWorkflowFireOutput { fired, skipped })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::entity::projection::{PluginDispatch, Projection};
use crate::domain::graph::{ulid_from_seed, Edge, Node};
use serde_json::json;
fn setup() -> SqliteStorage {
let s = SqliteStorage::open_in_memory().unwrap();
s.migrate().unwrap();
s.seed_default_types().unwrap();
s
}
fn default_registry() -> PluginRegistry {
PluginRegistry::default_for_wire().unwrap()
}
fn bare_node(id: &str, type_: &str) -> Node {
Node {
id: ulid_from_seed(id),
name: id.into(),
r#type: type_.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: json!({}),
}
}
#[test]
fn wire_init_with_no_projections_yields_empty() {
let s = setup();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.persona_id, "alpha");
assert!(out.projections.is_empty());
assert!(out.warnings.is_empty());
}
#[test]
fn wire_init_renders_registered_projection() {
let s = setup();
s.insert_node(&bare_node("alpha", "persona")).unwrap();
s.insert_node(&bare_node("beta", "persona")).unwrap();
SpecRegistry::new(&s)
.register("active_personas", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"_persona_toc",
"active_personas",
"Personas ({{count}}): {{names}}",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.projections.len(), 1);
let p = &out.projections[0];
assert_eq!(p.name, "_persona_toc");
assert_eq!(p.target_form, TargetForm::Prompt);
assert!(p.rendered.contains("Personas (2):"));
assert!(p.rendered.contains("beta"));
assert!(p.rendered.contains("alpha"));
assert!(out.warnings.is_empty());
}
#[test]
fn wire_init_warns_on_unknown_spec_ref() {
let s = setup();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"broken",
"no_such_spec",
"x",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert!(out.projections.is_empty());
assert_eq!(out.warnings.len(), 1);
assert!(out.warnings[0].contains("no_such_spec"));
}
#[test]
fn wire_close_reports_orphans_and_totals() {
let s = setup();
for id in ["a", "b", "c"] {
s.insert_node(&bare_node(id, "persona")).unwrap();
}
s.insert_edge(&Edge {
id: ulid_from_seed("e1"),
name: Some("e1".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
let out = wire_close(
WireCloseInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.total_node_count, 3);
assert_eq!(out.total_edge_count, 1);
assert_eq!(out.orphan_node_count, 1);
assert!(out
.report_markdown
.contains("orphan nodes (no edges, not self-attached): 1"));
assert!(out.report_markdown.contains("total nodes: 3"));
}
#[test]
fn graph_scan_excludes_self_attached_wiring_from_orphans() {
let s = setup();
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let mut n1 = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
n1.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new("mini-app://mailbox?alias=for_p").unwrap(),
None,
);
s.insert_node(&n1).unwrap();
let mut n2 = bare_node("p.priorities", wiring_mapper::WIRING_TYPE);
let mut extras = serde_json::Map::new();
extras.insert(wiring_mapper::META_MAINTENANCE_EXEMPT.into(), json!(true));
let mut metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("priorities").unwrap(),
&Source::new("placeholder://x").unwrap(),
Some(extras),
);
metadata
.as_object_mut()
.unwrap()
.remove(wiring_mapper::META_SOURCE_URI);
n2.metadata = metadata;
s.insert_node(&n2).unwrap();
s.insert_node(&bare_node("p", "persona")).unwrap();
let out = wire_doctor(&s, None, &default_registry()).unwrap();
let summary = graph_scan_summary(&s).unwrap();
assert_eq!(summary.total_node_count, 3);
assert_eq!(summary.total_edge_count, 0);
assert_eq!(
summary.orphan_node_count, 1,
"only the bare persona node is orphan; the 2 wiring entries are self-attached"
);
assert!(out.report_markdown.contains("scope: full"));
}
#[test]
fn wire_doctor_returns_2axis_integrated_report() {
let storage = setup();
let out = wire_doctor(&storage, None, &default_registry())
.expect("wire_doctor should pass on empty setup");
assert!(
out.report_markdown.contains("## Graph axis"),
"report_markdown should contain '## Graph axis' header"
);
assert!(
out.report_markdown.contains("## Workflow axis"),
"report_markdown should contain '## Workflow axis' header"
);
assert!(out.report_markdown.contains("scope: full"));
assert!(out.report_markdown.contains("verdict: BROKEN"));
assert!(out.report_markdown.contains("graph.edges_zero"));
}
#[test]
fn wire_doctor_report_includes_adapters_section() {
let storage = setup();
let out = wire_doctor(&storage, None, &default_registry()).unwrap();
assert!(
out.report_markdown.contains("## Adapters"),
"report_markdown should contain '## Adapters' header; got: {}",
out.report_markdown
);
assert!(
out.report_markdown
.contains("- file: lines, tail(n_max=1000)"),
"report_markdown should list the bundled FileAdapter's filter caps; got: {}",
out.report_markdown
);
}
#[test]
fn wire_close_empty_graph_zero_everything() {
let s = setup();
let out = wire_close(
WireCloseInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.total_node_count, 0);
assert_eq!(out.total_edge_count, 0);
assert_eq!(out.orphan_node_count, 0);
}
#[test]
fn wire_node_delete_returns_true_when_row_exists() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "node");
assert_eq!(out.id_or_name, "a");
assert!(out.deleted);
let out2 = wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert!(!out2.deleted);
}
#[test]
fn wire_node_delete_returns_false_when_row_missing() {
let s = setup();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "ghost".into(),
},
&s,
)
.unwrap();
assert!(!out.deleted);
}
#[test]
fn wire_edge_delete_returns_true_when_row_exists() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
s.insert_node(&bare_node("b", "persona")).unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e1"),
name: Some("e1".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
let out = wire_edge_delete(
WireDeleteInput {
id_or_name: "e1".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "edge");
assert!(out.deleted);
}
#[test]
fn wire_spec_delete_returns_true_when_row_exists() {
let s = setup();
SpecRegistry::new(&s)
.register("active_personas", &Specification::TypeIs("persona".into()))
.unwrap();
let out = wire_spec_delete(
WireDeleteInput {
id_or_name: "active_personas".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "spec");
assert!(out.deleted);
}
#[test]
fn wire_projection_delete_returns_true_when_row_exists() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"doomed",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_projection_delete(
WireDeleteInput {
id_or_name: "doomed".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "projection");
assert!(out.deleted);
assert!(ProjectionRegistry::new(&s).list().unwrap().is_empty());
}
#[test]
fn workflow_register_round_trips_via_list() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "alpha.workflow.review_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"emit_projection","projection_names":["review_pending"]}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_list(
WireWorkflowListInput {
persona_id: Some("alpha".into()),
trigger_kind: None,
enabled_only: None,
},
&s,
)
.unwrap();
assert_eq!(out.workflows.len(), 1);
let w = &out.workflows[0];
assert_eq!(w.id, "alpha.workflow.review_close");
assert_eq!(w.persona_id.as_deref(), Some("alpha"));
assert!(w.enabled);
assert_eq!(w.trigger["kind"], "on_event");
assert_eq!(w.action["kind"], "emit_projection");
}
#[test]
fn workflow_register_rejects_unsupported_trigger_kind() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"cron","cron_spec":"0 9 * * *"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("cron"));
}
#[test]
fn workflow_register_rejects_on_event_without_event_field() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"on_event"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("event"));
}
#[test]
fn workflow_register_rejects_emit_projection_without_names() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"emit_projection"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("projection_names"));
}
#[test]
fn workflow_list_filters_by_trigger_kind_and_enabled() {
let s = setup();
for (id, kind, enabled) in [
("w1", "on_demand", true),
("w2", "on_event", true),
("w3", "on_demand", false),
] {
let trig = if kind == "on_event" {
json!({"kind":"on_event","event":"e"})
} else {
json!({"kind":"on_demand"})
};
wire_workflow_register(
WireWorkflowRegisterInput {
id: id.into(),
persona_id: None,
trigger: trig,
action: json!({"kind":"no_op"}),
enabled: Some(enabled),
},
&s,
)
.unwrap();
}
let out = wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: Some("on_demand".into()),
enabled_only: None,
},
&s,
)
.unwrap();
let ids: Vec<&str> = out.workflows.iter().map(|w| w.id.as_str()).collect();
assert_eq!(ids, vec!["w1"]);
let out2 = wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: Some("on_demand".into()),
enabled_only: Some(false),
},
&s,
)
.unwrap();
let mut ids2: Vec<&str> = out2.workflows.iter().map(|w| w.id.as_str()).collect();
ids2.sort();
assert_eq!(ids2, vec!["w1", "w3"]);
}
#[test]
fn workflow_fire_by_id_returns_resolved_emit_projection() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "w1".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"emit_projection","projection_names":["slot_a","slot_b"]}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: Some("w1".into()),
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap();
assert_eq!(out.fired.len(), 1);
assert!(out.skipped.is_empty());
let f = &out.fired[0];
assert_eq!(f.id, "w1");
assert_eq!(f.action_kind, "emit_projection");
assert_eq!(
f.action_emit_projection_names.as_deref(),
Some(&["slot_a".to_string(), "slot_b".to_string()][..])
);
}
#[test]
fn workflow_fire_by_event_skips_unrelated_and_disabled() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "match_open".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_open"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "match_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "disabled_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"no_op"}),
enabled: Some(false),
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "demand_only".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: None,
event: Some("session_close".into()),
persona_id: Some("alpha".into()),
dry_run: None,
},
&s,
)
.unwrap();
let fired_ids: Vec<&str> = out.fired.iter().map(|f| f.id.as_str()).collect();
assert_eq!(fired_ids, vec!["match_close"]);
assert_eq!(out.skipped.len(), 3);
}
#[test]
fn workflow_fire_requires_exactly_one_of_id_or_event() {
let s = setup();
let err = wire_workflow_fire(
WireWorkflowFireInput {
id: None,
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("id"));
}
#[test]
fn workflow_fire_by_id_handles_missing() {
let s = setup();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: Some("ghost".into()),
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap();
assert!(out.fired.is_empty());
assert_eq!(out.skipped.len(), 1);
assert_eq!(out.skipped[0].0, "ghost");
}
#[test]
fn workflow_delete_uses_node_delete() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "w1".into(),
persona_id: None,
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "w1".into(),
},
&s,
)
.unwrap();
assert!(out.deleted);
assert!(wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: None,
enabled_only: Some(false),
},
&s,
)
.unwrap()
.workflows
.is_empty());
}
#[test]
fn wire_node_delete_cascades_to_referencing_edges() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
s.insert_node(&bare_node("b", "persona")).unwrap();
s.insert_node(&bare_node("c", "persona")).unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_ab"),
name: Some("e_ab".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_ca"),
name: Some("e_ca".into()),
src_node: ulid_from_seed("c"),
tgt_node: ulid_from_seed("a"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_bc"),
name: Some("e_bc".into()),
src_node: ulid_from_seed("b"),
tgt_node: ulid_from_seed("c"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert!(s.get_edge(&ulid_from_seed("e_ab")).unwrap().is_none());
assert!(s.get_edge(&ulid_from_seed("e_ca")).unwrap().is_none());
assert!(s.get_edge(&ulid_from_seed("e_bc")).unwrap().is_some());
}
#[test]
fn wire_init_rejects_non_static_projection_kind() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"async_only",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "llm", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let result = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-static projection_kind to fail"),
};
assert!(err.contains("async_only"), "err: {err}");
assert!(err.contains("llm"), "err: {err}");
assert!(err.contains("wire_prompt_context"), "err: {err}");
}
#[test]
fn wire_render_rejects_non_static_projection_kind() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"summarized",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "cache", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let result = wire_render(
WireRenderInput {
projection_ref: "summarized".into(),
},
&s,
&default_registry(),
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-static projection_kind to fail"),
};
assert!(err.contains("summarized"), "err: {err}");
assert!(err.contains("cache"), "err: {err}");
assert!(err.contains("wire_prompt_context"), "err: {err}");
}
#[test]
fn wire_init_accepts_explicit_static_projection_kind() {
let s = setup();
s.insert_node(&bare_node("alpha", "persona")).unwrap();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"explicit_static",
"p",
"n={{count}}",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "static", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.projections.len(), 1);
assert_eq!(out.projections[0].rendered, "n=1");
}
fn seed_wiring_node(s: &SqliteStorage, id: &str, source_uri: &str) {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
s.insert_node(&Node {
id: ulid_from_seed(id),
name: id.into(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: Some(1.0),
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: wiring_mapper::wiring_metadata_object(
&PersonaId::new("alice").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new(source_uri).unwrap(),
None,
),
})
.unwrap();
}
#[test]
fn node_update_merge_overwrites_one_key_preserves_others() {
let s = setup();
seed_wiring_node(&s, "alice.mailbox", "mini-app://mailbox?alias=for_alice");
let out = wire_node_update(
WireNodeUpdateInput {
id: "alice.mailbox".into(),
metadata_patch: json!({
"source_uri": "mini-app://mailbox?alias=for_alice&limit=10",
}),
mode: WireNodeUpdateMode::Merge,
},
&s,
)
.unwrap();
use crate::application::wiring_mapper;
assert_eq!(out.id, "alice.mailbox");
assert_eq!(out.mode, WireNodeUpdateMode::Merge);
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert_eq!(
wiring_mapper::extract_source_uri(&synthetic),
Some("mini-app://mailbox?alias=for_alice&limit=10")
);
assert_eq!(wiring_mapper::extract_persona(&synthetic), Some("alice"));
assert_eq!(wiring_mapper::extract_slot(&synthetic), Some("mailbox"));
let stored = s.get_node_by_name("alice.mailbox").unwrap().unwrap();
assert_eq!(
wiring_mapper::extract_source_uri(&stored),
Some("mini-app://mailbox?alias=for_alice&limit=10")
);
}
#[test]
fn node_update_merge_null_value_deletes_key() {
use crate::application::wiring_mapper;
let s = setup();
seed_wiring_node(&s, "alice.tmp", "mini-app://x");
let out = wire_node_update(
WireNodeUpdateInput {
id: "alice.tmp".into(),
metadata_patch: json!({ wiring_mapper::META_SLOT: null }),
mode: WireNodeUpdateMode::Merge,
},
&s,
)
.unwrap();
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert!(wiring_mapper::extract_slot(&synthetic).is_none());
assert_eq!(wiring_mapper::extract_persona(&synthetic), Some("alice"));
assert_eq!(
wiring_mapper::extract_source_uri(&synthetic),
Some("mini-app://x")
);
}
#[test]
fn node_update_replace_swaps_metadata_wholesale() {
let s = setup();
seed_wiring_node(&s, "alice.tmp", "mini-app://x");
let out = wire_node_update(
WireNodeUpdateInput {
id: "alice.tmp".into(),
metadata_patch: json!({"only_field": 42}),
mode: WireNodeUpdateMode::Replace,
},
&s,
)
.unwrap();
use crate::application::wiring_mapper;
assert_eq!(out.metadata, json!({"only_field": 42}));
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert!(wiring_mapper::extract_persona(&synthetic).is_none());
}
#[test]
fn node_update_unknown_id_returns_not_found() {
let s = setup();
let result = wire_node_update(
WireNodeUpdateInput {
id: "does.not.exist".into(),
metadata_patch: json!({"x": 1}),
mode: WireNodeUpdateMode::Merge,
},
&s,
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected NotFound"),
};
assert!(err.contains("does.not.exist"), "err: {err}");
}
#[test]
fn node_update_rejects_non_object_patch() {
let s = setup();
seed_wiring_node(&s, "alice.tmp", "mini-app://x");
let result = wire_node_update(
WireNodeUpdateInput {
id: "alice.tmp".into(),
metadata_patch: json!("not an object"),
mode: WireNodeUpdateMode::Merge,
},
&s,
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-object patch to fail"),
};
assert!(err.contains("must be a JSON object"), "err: {err}");
}
#[test]
fn node_update_mode_parse_rejects_unknown() {
assert_eq!(
WireNodeUpdateMode::parse("merge").unwrap(),
WireNodeUpdateMode::Merge
);
assert_eq!(
WireNodeUpdateMode::parse("replace").unwrap(),
WireNodeUpdateMode::Replace
);
assert!(WireNodeUpdateMode::parse("upsert").is_err());
}
fn seed_wiring(
s: &SqliteStorage,
persona: &str,
slot: &str,
source_uri: &str,
maintenance_exempt: bool,
) {
let meta = if maintenance_exempt {
json!({
"persona": persona,
"axis": slot,
"source_uri": source_uri,
"maintenance_exempt": true,
})
} else {
json!({
"persona": persona,
"axis": slot,
"source_uri": source_uri,
})
};
let mut n = bare_node(&format!("{persona}.{slot}"), "outline_node");
n.metadata = meta;
s.insert_node(&n).unwrap();
}
#[test]
fn context_get_returns_wirings_and_workflows_for_persona() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
seed_wiring(&s, "alpha", "mail", "mini-app://mail?alias=for_alpha", true);
seed_wiring(
&s,
"beta",
"mailbox",
"mini-app://mailbox?alias=for_beta",
false,
);
wire_workflow_register(
WireWorkflowRegisterInput {
id: "alpha.workflow.session_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"emit_projection","projection_names":["mailbox"]}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "beta.workflow.session_close".into(),
persona_id: Some("beta".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.persona_id, "alpha");
assert_eq!(out.wirings.len(), 2);
assert_eq!(out.wirings[0].slot, "mail");
assert!(out.wirings[0].maintenance_exempt);
assert_eq!(out.wirings[1].slot, "mailbox");
assert!(!out.wirings[1].maintenance_exempt);
assert_eq!(out.workflows.len(), 1);
assert_eq!(out.workflows[0].id, "alpha.workflow.session_close");
assert_eq!(out.workflows[0].persona_id.as_deref(), Some("alpha"));
}
#[test]
fn context_get_resolves_projection_ref_via_naming_convention() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"alpha.section.mailbox",
"spec_ignored_here",
"tpl",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert_eq!(
out.wirings[0].projection_ref.as_deref(),
Some("alpha.section.mailbox"),
"projection_ref must resolve via <persona>.section.<slot> naming convention",
);
}
#[test]
fn context_get_leaves_projection_ref_none_when_not_registered() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert!(out.wirings[0].projection_ref.is_none());
}
#[test]
fn context_get_returns_empty_for_unknown_persona() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "ghost".into(),
},
&s,
)
.unwrap();
assert_eq!(out.persona_id, "ghost");
assert!(out.wirings.is_empty());
assert!(out.workflows.is_empty());
}
#[test]
fn context_get_rejects_empty_persona_id() {
let s = setup();
let err = wire_context_get(
WireContextGetInput {
persona_id: String::new(),
},
&s,
)
.expect_err("empty persona id must reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidPersonaId(_))
));
}
#[test]
fn context_get_skips_drift_wiring_nodes_silently() {
let s = setup();
let mut drift = bare_node("alpha.mailbox", "outline_node");
drift.metadata = json!({
"persona": "alpha",
"axis": "mailbox",
});
s.insert_node(&drift).unwrap();
seed_wiring(
&s,
"alpha",
"mail",
"mini-app://mail?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert_eq!(out.wirings[0].slot, "mail");
}
fn seed_three_slots(s: &SqliteStorage) {
seed_wiring(s, "alpha", "news", "mini-app://news?alias=for_alpha", false);
seed_wiring(s, "alpha", "mail", "mini-app://mail?alias=for_alpha", false);
seed_wiring(s, "alpha", "todo", "mini-app://todo?alias=for_alpha", false);
}
fn sorted(mut v: Vec<String>) -> Vec<String> {
v.sort();
v
}
#[test]
fn enumerate_slots_both_none_returns_all() {
let s = setup();
seed_three_slots(&s);
let got = enumerate_slot_names(&s, "alpha", None, None).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn enumerate_slots_include_only_returns_explicit_set() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), None).unwrap();
assert_eq!(got, vec!["news".to_string(), "mail".into()]);
}
#[test]
fn enumerate_slots_exclude_only_subtracts_from_all() {
let s = setup();
seed_three_slots(&s);
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", None, Some(&exclude)).unwrap();
assert_eq!(sorted(got), vec!["news".to_string(), "todo".into()]);
}
#[test]
fn enumerate_slots_both_include_and_exclude_and_not() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into(), "todo".into()];
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert_eq!(got, vec!["news".to_string(), "todo".into()]);
}
#[test]
fn enumerate_slots_intersection_exclude_wins() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert_eq!(got, vec!["news".to_string()]);
}
#[test]
fn enumerate_slots_unknown_exclude_name_is_ignored() {
let s = setup();
seed_three_slots(&s);
let exclude = vec!["nonexistent".to_string()];
let got = enumerate_slot_names(&s, "alpha", None, Some(&exclude)).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn enumerate_slots_empty_result_returns_empty_vec() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let exclude = vec!["news".to_string(), "mail".into()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert!(got.is_empty());
}
#[test]
fn enumerate_slots_exclude_empty_vec_is_noop() {
let s = setup();
seed_three_slots(&s);
let empty: Vec<String> = vec![];
let got = enumerate_slot_names(&s, "alpha", None, Some(&empty)).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn merge_auth_query_no_metadata_auth_leaves_uri_unchanged() {
assert_eq!(
merge_auth_query("github://octocat/hello-world", None),
"github://octocat/hello-world"
);
}
#[test]
fn merge_auth_query_appends_when_uri_has_no_query() {
assert_eq!(
merge_auth_query("github://octocat/hello-world", Some("github-alt")),
"github://octocat/hello-world?auth=github-alt"
);
}
#[test]
fn merge_auth_query_appends_with_ampersand_when_uri_already_has_query() {
assert_eq!(
merge_auth_query(
"github://octocat/hello-world?kind=issues",
Some("github-alt")
),
"github://octocat/hello-world?kind=issues&auth=github-alt"
);
}
#[test]
fn merge_auth_query_uri_side_auth_wins_no_overwrite() {
assert_eq!(
merge_auth_query(
"github://octocat/hello-world?auth=from-uri",
Some("from-metadata")
),
"github://octocat/hello-world?auth=from-uri"
);
}
#[test]
fn merge_auth_query_preserves_fragment_after_merged_query() {
assert_eq!(
merge_auth_query("file:///tmp/x#frag", Some("svc")),
"file:///tmp/x?auth=svc#frag"
);
}
#[test]
fn merge_auth_query_unparsable_uri_left_unchanged() {
assert_eq!(
merge_auth_query("not-a-uri", Some("svc")),
"not-a-uri",
"unparsable source_uri must pass through unchanged"
);
}
#[test]
fn append_query_param_uses_question_mark_when_absent() {
assert_eq!(
append_query_param("mini-app://mailbox", "auth", "k"),
"mini-app://mailbox?auth=k"
);
}
#[test]
fn append_query_param_uses_ampersand_when_query_present() {
assert_eq!(
append_query_param("mini-app://mailbox?alias=x", "auth", "k"),
"mini-app://mailbox?alias=x&auth=k"
);
}
fn register_stub_projection(s: &SqliteStorage, name: &str) {
use crate::domain::entity::projection::{PluginDispatch, Projection};
ProjectionRegistry::new(s)
.register(
&Projection::from_parts(
name,
"unused_spec_ref",
"n={{count}}",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
}
#[test]
fn collect_slot_extracts_auth_from_wiring_metadata() {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let s = setup();
let mut extras = serde_json::Map::new();
extras.insert("auth".to_string(), json!("svc-x"));
let mut node = bare_node("p.issues", wiring_mapper::WIRING_TYPE);
node.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("issues").unwrap(),
&Source::new("github://o/r").unwrap(),
Some(extras),
);
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.issues");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected = collect_slot("issues", "p", &s, &proj_reg, &overlays, &mut warnings)
.unwrap()
.expect("wiring entry should collect");
assert_eq!(collected.source_uri, "github://o/r");
assert_eq!(collected.auth.as_deref(), Some("svc-x"));
assert!(warnings.is_empty(), "warnings: {warnings:?}");
}
#[test]
fn collect_slot_auth_none_when_metadata_lacks_auth() {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let s = setup();
let mut node = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
node.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new("mini-app://mailbox").unwrap(),
None,
);
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.mailbox");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected = collect_slot("mailbox", "p", &s, &proj_reg, &overlays, &mut warnings)
.unwrap()
.expect("wiring entry should collect");
assert_eq!(collected.auth, None);
}
fn slot_register_input(
persona: &str,
slot: &str,
uri: &str,
template: &str,
) -> WireSlotRegisterInput {
WireSlotRegisterInput {
persona_id: persona.into(),
slot: slot.into(),
source_uri: uri.into(),
template: template.into(),
target_form: TargetForm::Markdown,
maintenance_exempt: None,
auth: None,
}
}
#[test]
fn wire_slot_register_creates_node_spec_and_projection() {
use crate::application::wiring_mapper;
let s = setup();
let out = wire_slot_register(
slot_register_input("alpha", "notes", "file:~/notes.md", "## Notes\n{{count}}"),
&s,
)
.unwrap();
assert_eq!(out.node_name, "alpha.notes");
assert!(out.node_created);
assert_eq!(out.spec_name, "alpha.spec.notes");
assert_eq!(out.projection_name, "alpha.section.notes");
let node = s.get_node_by_name("alpha.notes").unwrap().expect("node");
assert_eq!(wiring_mapper::extract_persona(&node), Some("alpha"));
assert_eq!(wiring_mapper::extract_slot(&node), Some("notes"));
assert_eq!(
wiring_mapper::extract_source_uri(&node),
Some("file:~/notes.md")
);
let spec = SpecRegistry::new(&s)
.get("alpha.spec.notes")
.unwrap()
.expect("spec");
assert!(matches!(spec, Specification::And(parts) if parts.len() == 3));
let proj = ProjectionRegistry::new(&s)
.get("alpha.section.notes")
.unwrap()
.expect("projection");
assert_eq!(proj.template().as_str(), "## Notes\n{{count}}");
assert_eq!(proj.spec_ref().as_str(), "alpha.spec.notes");
}
#[test]
fn wire_slot_register_upserts_in_place_preserving_node_id() {
use crate::application::wiring_mapper;
let s = setup();
let first = wire_slot_register(
slot_register_input("alpha", "notes", "file:~/a.md", "v1 {{count}}"),
&s,
)
.unwrap();
let node = s.get_node_by_name("alpha.notes").unwrap().unwrap();
let mut meta = node.metadata.as_object().cloned().unwrap();
meta.insert("custom_flag".into(), json!(true));
s.update_node_metadata(&node.id, &serde_json::Value::Object(meta))
.unwrap();
let second = wire_slot_register(
WireSlotRegisterInput {
maintenance_exempt: Some(true),
..slot_register_input("alpha", "notes", "file:~/b.md", "v2 {{count}}")
},
&s,
)
.unwrap();
assert!(!second.node_created, "second call must be an upsert");
assert_eq!(first.node_id, second.node_id, "node ULID preserved");
let node = s.get_node_by_name("alpha.notes").unwrap().unwrap();
assert_eq!(
wiring_mapper::extract_source_uri(&node),
Some("file:~/b.md"),
"canonical key overwritten"
);
assert!(
wiring_mapper::extract_maintenance_exempt(&node),
"maintenance_exempt applied"
);
assert_eq!(
node.metadata.get("custom_flag"),
Some(&json!(true)),
"passthrough key kept"
);
let proj = ProjectionRegistry::new(&s)
.get("alpha.section.notes")
.unwrap()
.unwrap();
assert_eq!(proj.template().as_str(), "v2 {{count}}");
}
#[test]
fn wire_slot_register_rejects_invalid_slot() {
let s = setup();
let err = wire_slot_register(slot_register_input("alpha", "a.b", "file:~/x.md", "t"), &s)
.expect_err("dotted slot must reject");
assert!(err.to_string().contains("."), "err: {err}");
assert!(s.get_node_by_name("alpha.a.b").unwrap().is_none());
}
#[test]
fn wire_slot_delete_removes_all_three_and_is_idempotent() {
let s = setup();
wire_slot_register(
slot_register_input("alpha", "notes", "file:~/n.md", "{{count}}"),
&s,
)
.unwrap();
let del = wire_slot_delete(
WireSlotDeleteInput {
persona_id: "alpha".into(),
slot: "notes".into(),
},
&s,
)
.unwrap();
assert!(del.node_deleted && del.spec_deleted && del.projection_deleted);
assert!(s.get_node_by_name("alpha.notes").unwrap().is_none());
assert!(SpecRegistry::new(&s)
.get("alpha.spec.notes")
.unwrap()
.is_none());
assert!(ProjectionRegistry::new(&s)
.get("alpha.section.notes")
.unwrap()
.is_none());
let again = wire_slot_delete(
WireSlotDeleteInput {
persona_id: "alpha".into(),
slot: "notes".into(),
},
&s,
)
.unwrap();
assert!(
!again.node_deleted && !again.spec_deleted && !again.projection_deleted,
"second delete reports false everywhere"
);
}
#[test]
fn collect_slot_honors_explicit_projection_ref_over_convention() {
use crate::application::wiring_mapper;
let s = setup();
let mut node = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
node.metadata = json!({
"persona": "p",
"axis": "mailbox",
"source_uri": "mini-app://mailbox",
"projection_ref": "shared.section.mailbox",
});
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.mailbox");
register_stub_projection(&s, "shared.section.mailbox");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected = collect_slot("mailbox", "p", &s, &proj_reg, &overlays, &mut warnings)
.unwrap()
.expect("wiring entry should collect");
assert_eq!(collected.projection_name, "shared.section.mailbox");
assert!(warnings.is_empty(), "warnings: {warnings:?}");
}
#[test]
fn collect_slot_missing_explicit_projection_ref_warns_and_skips() {
use crate::application::wiring_mapper;
let s = setup();
let mut node = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
node.metadata = json!({
"persona": "p",
"axis": "mailbox",
"source_uri": "mini-app://mailbox",
"projection_ref": "nowhere.section.mailbox",
});
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.mailbox");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected =
collect_slot("mailbox", "p", &s, &proj_reg, &overlays, &mut warnings).unwrap();
assert!(collected.is_none(), "slot must skip");
assert!(
warnings
.iter()
.any(|w| w.contains("nowhere.section.mailbox")),
"warning names the missing explicit ref: {warnings:?}"
);
}
#[tokio::test]
async fn wire_fetch_returns_raw_adapter_output_for_file_uri() {
let dir = std::env::temp_dir().join(format!("wire-fetch-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("body.md");
std::fs::write(&file, "hello wire_fetch").unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = default_registry();
let out = wire_fetch(
WireFetchInput {
source_uri: Some(format!("file:{}", file.display())),
persona_id: None,
slot: None,
},
s,
®istry,
)
.await
.unwrap();
assert_eq!(out.fetched_data["body"], json!("hello wire_fetch"));
assert_eq!(out.fetched_data["scheme"], json!("file"));
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn wire_fetch_resolves_wiring_entry_by_persona_and_slot() {
let dir = std::env::temp_dir().join(format!("wire-fetch-slot-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("notes.md");
std::fs::write(&file, "slot preview").unwrap();
let storage = setup();
wire_slot_register(
slot_register_input(
"alpha",
"notes",
&format!("file:{}", file.display()),
"{{count}}",
),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = default_registry();
let out = wire_fetch(
WireFetchInput {
source_uri: None,
persona_id: Some("alpha".into()),
slot: Some("notes".into()),
},
s,
®istry,
)
.await
.unwrap();
assert_eq!(out.fetched_data["body"], json!("slot preview"));
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn wire_fetch_rejects_ambiguous_or_empty_input() {
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = default_registry();
let err = wire_fetch(
WireFetchInput {
source_uri: None,
persona_id: None,
slot: None,
},
s.clone(),
®istry,
)
.await
.expect_err("empty input must reject");
assert!(err.to_string().contains("wire_fetch"), "err: {err}");
let err = wire_fetch(
WireFetchInput {
source_uri: Some("file:~/x.md".into()),
persona_id: Some("alpha".into()),
slot: Some("notes".into()),
},
s,
®istry,
)
.await
.expect_err("both forms at once must reject");
assert!(err.to_string().contains("wire_fetch"), "err: {err}");
}
struct ListyAdapter;
#[async_trait::async_trait]
impl Adapter for ListyAdapter {
fn scheme(&self) -> &'static str {
"listy"
}
fn filter_caps(&self) -> &'static [crate::infrastructure::filter::FilterCap] {
&[crate::infrastructure::filter::FilterCap::Limit { max: None }]
}
async fn fetch(&self, uri: &WireUri) -> WireResult<serde_json::Value> {
let _ = WireFilters::parse(uri, self.filter_caps())?;
Ok(listy_raw_output())
}
}
fn listy_raw_output() -> serde_json::Value {
json!({
"scheme": "listy",
"items": [
{"title": "alpha item"},
{"title": "beta item"},
{"title": "beta second"},
],
"has_more": false,
})
}
struct PassthroughAdapter;
#[async_trait::async_trait]
impl Adapter for PassthroughAdapter {
fn scheme(&self) -> &'static str {
"passx"
}
fn post_filterable(&self) -> bool {
false
}
async fn fetch(&self, uri: &WireUri) -> WireResult<serde_json::Value> {
Ok(json!({ "saw_query": uri.query_get("query") }))
}
}
#[tokio::test]
async fn wire_fetch_post_filters_undeclared_query_and_marks() {
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = PluginRegistry::builder()
.with_adapter(ListyAdapter)
.build()
.unwrap();
let out = wire_fetch(
WireFetchInput {
source_uri: Some("listy://h?query=BETA".into()),
persona_id: None,
slot: None,
},
s,
®istry,
)
.await
.unwrap();
let items = out.fetched_data["items"].as_array().unwrap();
assert_eq!(items.len(), 2, "narrowed case-insensitively: {items:?}");
assert_eq!(out.fetched_data["post_filtered"], json!(["query"]));
}
#[tokio::test]
async fn wire_fetch_native_request_stays_byte_identical_without_marker() {
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = PluginRegistry::builder()
.with_adapter(ListyAdapter)
.build()
.unwrap();
let out = wire_fetch(
WireFetchInput {
source_uri: Some("listy://h?limit=2".into()),
persona_id: None,
slot: None,
},
s,
®istry,
)
.await
.unwrap();
assert_eq!(
out.fetched_data,
listy_raw_output(),
"declared-cap request must return the adapter output verbatim (no marker)"
);
}
#[tokio::test]
async fn wire_fetch_post_query_on_document_shape_fails_loud() {
let dir = std::env::temp_dir().join(format!("wire-postq-doc-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("doc.md");
std::fs::write(&file, "some text").unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = default_registry();
let err = wire_fetch(
WireFetchInput {
source_uri: Some(format!("file:{}?query=text", file.display())),
persona_id: None,
slot: None,
},
s,
®istry,
)
.await
.expect_err("query post-filter on a document shape must fail loud");
assert!(err.to_string().contains("items"), "err: {err}");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn wire_fetch_optout_adapter_keeps_vocabulary_keys() {
let s = std::sync::Arc::new(std::sync::Mutex::new(setup()));
let registry = PluginRegistry::builder()
.with_adapter(PassthroughAdapter)
.build()
.unwrap();
let out = wire_fetch(
WireFetchInput {
source_uri: Some("passx://h?query=abc".into()),
persona_id: None,
slot: None,
},
s,
®istry,
)
.await
.unwrap();
assert_eq!(
out.fetched_data,
json!({ "saw_query": "abc" }),
"opt-out adapter must see the key untouched and gain no marker"
);
}
#[tokio::test]
async fn wire_prompt_context_renders_post_filtered_items() {
let storage = setup();
wire_slot_register(
slot_register_input(
"alpha",
"feed",
"listy://h?query=beta",
"{{#each entries}}{{#each this.fetched_data.items}}{{this.title}};{{/each}}{{/each}}",
),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::default_builder_for_wire()
.with_adapter(ListyAdapter)
.build()
.unwrap();
let out = wire_prompt_context(
WirePromptContextInput {
persona_id: "alpha".into(),
projection_names: None,
projection_exclude_names: None,
},
s,
®istry,
)
.await
.unwrap();
assert!(
out.prompt_context.contains("beta item;beta second;"),
"post-narrowed items render: {}",
out.prompt_context
);
assert!(
!out.prompt_context.contains("alpha item"),
"filtered-out item must not render: {}",
out.prompt_context
);
assert!(out.warnings.is_empty(), "warnings: {:?}", out.warnings);
}
#[test]
fn strip_query_params_removes_only_listed_keys() {
assert_eq!(
strip_query_params("x://h/p?query=a&limit=3", &["query"]),
"x://h/p?limit=3"
);
}
#[test]
fn strip_query_params_drops_question_mark_when_empty() {
assert_eq!(strip_query_params("x://h/p?query=a", &["query"]), "x://h/p");
}
#[test]
fn strip_query_params_preserves_fragment_and_no_query_uri() {
assert_eq!(
strip_query_params("x://h/p?query=a&k=v#frag", &["query"]),
"x://h/p?k=v#frag"
);
assert_eq!(
strip_query_params("x://h/p#frag", &["query"]),
"x://h/p#frag"
);
}
#[tokio::test]
async fn wire_prompt_context_warns_on_empty_render_with_non_null_fetch() {
let dir = std::env::temp_dir().join(format!("wire-empty-warn-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("data.md");
std::fs::write(&file, "real content").unwrap();
let storage = setup();
wire_slot_register(
slot_register_input(
"alpha",
"notes",
&format!("file:{}", file.display()),
"{{#each entries}}{{this.fetched_data.content}}{{/each}}",
),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = default_registry();
let out = wire_prompt_context(
WirePromptContextInput {
persona_id: "alpha".into(),
projection_names: None,
projection_exclude_names: None,
},
s,
®istry,
)
.await
.unwrap();
assert!(
out.warnings.iter().any(|w| w.contains("rendered empty")),
"warnings: {:?}",
out.warnings
);
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn wire_prompt_context_no_warning_when_template_renders_content() {
let dir = std::env::temp_dir().join(format!("wire-nonempty-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("data.md");
std::fs::write(&file, "real content").unwrap();
let storage = setup();
wire_slot_register(
slot_register_input(
"alpha",
"notes",
&format!("file:{}", file.display()),
"{{#each entries}}{{this.fetched_data.body}}{{/each}}",
),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = default_registry();
let out = wire_prompt_context(
WirePromptContextInput {
persona_id: "alpha".into(),
projection_names: None,
projection_exclude_names: None,
},
s,
®istry,
)
.await
.unwrap();
assert!(
out.warnings.is_empty(),
"no warnings expected: {:?}",
out.warnings
);
assert!(out.prompt_context.contains("real content"));
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn wire_prompt_context_renders_through_explicit_projection_ref() {
use crate::application::wiring_mapper;
use crate::domain::entity::projection::{PluginDispatch, Projection};
let dir = std::env::temp_dir().join(format!("wire-projref-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("shared.md");
std::fs::write(&file, "shared body").unwrap();
let storage = setup();
ProjectionRegistry::new(&storage)
.register(
&Projection::from_parts(
"shared.section.notes",
"unused_spec_ref",
"SHARED: {{#each entries}}{{this.fetched_data.body}}{{/each}}",
TargetForm::Markdown,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let mut node = bare_node("beta.notes", wiring_mapper::WIRING_TYPE);
node.metadata = json!({
"persona": "beta",
"axis": "notes",
"source_uri": format!("file:{}", file.display()),
"projection_ref": "shared.section.notes",
});
storage.insert_node(&node).unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = default_registry();
let out = wire_prompt_context(
WirePromptContextInput {
persona_id: "beta".into(),
projection_names: None,
projection_exclude_names: None,
},
s,
®istry,
)
.await
.unwrap();
assert!(
out.prompt_context.contains("SHARED: shared body"),
"rendered: {}",
out.prompt_context
);
assert_eq!(out.projections[0].name, "shared.section.notes");
std::fs::remove_dir_all(&dir).ok();
}
struct StubItemsAdapter;
#[async_trait::async_trait]
impl Adapter for StubItemsAdapter {
fn scheme(&self) -> &'static str {
"stub"
}
async fn fetch(&self, _uri: &WireUri) -> WireResult<serde_json::Value> {
Ok(json!({
"items": [
{"id": "m1", "body": "first"},
{"id": "m2", "body": "second"},
]
}))
}
}
fn tank_query_default() -> crate::infrastructure::storage::TankQuery {
crate::infrastructure::storage::TankQuery::default()
}
#[tokio::test]
async fn wire_materialize_persists_snapshot_items_registry_and_edge() {
let storage = setup();
wire_slot_register(
slot_register_input("alpha", "mailbox", "stub://mailbox", "{{count}}"),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::builder()
.with_adapter(StubItemsAdapter)
.build()
.unwrap();
let out = wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "mailbox".into(),
item_path: Some("/items".into()),
item_id_key: Some("id".into()),
},
s.clone(),
®istry,
)
.await
.unwrap();
assert_eq!(out.tank_uri, "tank://alpha/mailbox");
assert_eq!(out.item_count, 2);
assert_eq!(out.new_item_count, 2);
assert_eq!(out.deduped_count, 0);
assert!(out.registry_created);
let g = s.lock().unwrap();
let reg = g
.get_node_by_name("alpha.tank.mailbox")
.unwrap()
.expect("registry node");
assert_eq!(reg.r#type, "snapshot_registry");
assert_eq!(reg.metadata["source_uri"], "tank://alpha/mailbox");
assert_eq!(reg.metadata["upstream"], "stub://mailbox");
assert_eq!(reg.metadata["item_path"], "/items");
assert_eq!(reg.metadata["item_id_key"], "id");
assert_eq!(reg.metadata["prov"]["wasDerivedFrom"], "stub://mailbox");
let wiring = g.get_node_by_name("alpha.mailbox").unwrap().unwrap();
let edges = g.list_edges_from(®.id).unwrap();
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].kind, "archives");
assert_eq!(edges[0].tgt_node, wiring.id);
let (items, _) = g
.tank_query_items("alpha/mailbox", &tank_query_default())
.unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].identity, "m1");
assert_eq!(items[1].identity, "m2");
}
#[tokio::test]
async fn wire_materialize_second_run_dedups_and_stays_idempotent() {
let storage = setup();
wire_slot_register(
slot_register_input("alpha", "mailbox", "stub://mailbox", "{{count}}"),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::builder()
.with_adapter(StubItemsAdapter)
.build()
.unwrap();
let first = wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "mailbox".into(),
item_path: Some("/items".into()),
item_id_key: Some("id".into()),
},
s.clone(),
®istry,
)
.await
.unwrap();
assert_eq!(first.new_item_count, 2);
assert!(first.registry_created);
let second = wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "mailbox".into(),
item_path: None,
item_id_key: None,
},
s.clone(),
®istry,
)
.await
.unwrap();
assert_eq!(second.item_count, 2);
assert_eq!(second.new_item_count, 0, "same fetch → nothing new");
assert_eq!(second.deduped_count, 2);
assert!(!second.registry_created, "registry node reused");
let g = s.lock().unwrap();
let reg = g.get_node_by_name("alpha.tank.mailbox").unwrap().unwrap();
assert_eq!(
g.list_edges_from(®.id).unwrap().len(),
1,
"archives edge not duplicated"
);
let (items, _) = g
.tank_query_items("alpha/mailbox", &tank_query_default())
.unwrap();
assert_eq!(items.len(), 2, "timeline still holds 2 items");
}
#[tokio::test]
async fn wire_materialize_bad_item_path_fails_loud() {
let storage = setup();
wire_slot_register(
slot_register_input("alpha", "mailbox", "stub://mailbox", "{{count}}"),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::builder()
.with_adapter(StubItemsAdapter)
.build()
.unwrap();
let err = wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "mailbox".into(),
item_path: Some("/nonexistent".into()),
item_id_key: None,
},
s,
®istry,
)
.await
.expect_err("bad JSON pointer must fail loud");
assert!(err.to_string().contains("item_path"), "err: {err}");
}
#[tokio::test]
async fn wire_materialize_rejects_tank_source_loop() {
let storage = setup();
wire_slot_register(
slot_register_input("alpha", "archive", "tank://alpha/mailbox", "{{count}}"),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::builder()
.with_adapter(StubItemsAdapter)
.with_adapter(crate::infrastructure::tank::TankAdapter::new(s.clone()))
.build()
.unwrap();
let err = wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "archive".into(),
item_path: None,
item_id_key: None,
},
s.clone(),
®istry,
)
.await
.expect_err("materializing a tank:// into itself must fail loud");
assert!(err.to_string().contains("itself"), "err: {err}");
}
#[tokio::test]
async fn wire_materialize_then_wire_fetch_tank_reads_items() {
let storage = setup();
wire_slot_register(
slot_register_input("alpha", "mailbox", "stub://mailbox", "{{count}}"),
&storage,
)
.unwrap();
let s = std::sync::Arc::new(std::sync::Mutex::new(storage));
let registry = PluginRegistry::builder()
.with_adapter(StubItemsAdapter)
.with_adapter(crate::infrastructure::tank::TankAdapter::new(s.clone()))
.build()
.unwrap();
wire_materialize(
WireMaterializeInput {
persona_id: "alpha".into(),
slot: "mailbox".into(),
item_path: Some("/items".into()),
item_id_key: Some("id".into()),
},
s.clone(),
®istry,
)
.await
.unwrap();
let out = wire_fetch(
WireFetchInput {
source_uri: Some("tank://alpha/mailbox?tail_n=1".into()),
persona_id: None,
slot: None,
},
s.clone(),
®istry,
)
.await
.unwrap();
assert_eq!(out.fetched_data["kind"], "tank_items");
let items = out.fetched_data["items"].as_array().unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0]["identity"], "m2", "last item on the timeline");
assert_eq!(items[0]["payload"], json!({"id": "m2", "body": "second"}));
}
}