Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion artifacts/architecture.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ artifacts:
- id: ARCH-CORE-001
type: aadl-component
title: RivetCore process
description: >
description: |
Core library process containing all domain logic: config loading,
schema merging, artifact storage, adapter dispatch, graph building,
validation, matrix computation, diff, documents, and query.

```mermaid
flowchart LR
Config --> Store
Store --> Graph
Graph --> Validate
```
status: implemented
tags: [aadl, architecture, core]
links:
Expand Down
62 changes: 62 additions & 0 deletions rivet-cli/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,68 @@ pub fn list_topics(format: &str) -> String {
out
}

/// List every registered computed embed token.
///
/// Sourced from `rivet_core::embed::EMBED_REGISTRY` so the listing never
/// drifts from what `resolve_embed` actually dispatches.
pub fn list_embeds(format: &str) -> String {
let specs = rivet_core::embed::registry();

if format == "json" {
let items: Vec<serde_json::Value> = specs
.iter()
.map(|s| {
serde_json::json!({
"name": s.name,
"args": s.args,
"summary": s.summary,
"example": s.example,
"legacy": s.legacy,
})
})
.collect();
return serde_json::to_string_pretty(&serde_json::json!({
"command": "docs-embeds",
"embeds": items,
}))
.unwrap_or_default();
}

// Plain-text: aligned columns with a short footer pointing to the
// full syntax reference and to `rivet embed` for CLI rendering.
let name_w = specs.iter().map(|s| s.name.len()).max().unwrap_or(4);
let args_w = specs.iter().map(|s| s.args.len()).max().unwrap_or(6);
let mut out = String::new();
out.push_str("Registered computed embeds:\n\n");
out.push_str(&format!(
" {:<nw$} {:<aw$} SUMMARY\n",
"NAME",
"ARGS",
nw = name_w,
aw = args_w
));
for s in specs {
let marker = if s.legacy { " (inline)" } else { "" };
out.push_str(&format!(
" {:<nw$} {:<aw$} {}{}\n",
s.name,
s.args,
s.summary,
marker,
nw = name_w,
aw = args_w
));
}
out.push_str("\nExamples:\n");
for s in specs {
out.push_str(&format!(" {:<nw$} {}\n", s.name, s.example, nw = name_w));
}
out.push_str("\nUsage:\n");
out.push_str(" rivet embed <NAME>[:args] Render any embed from the CLI\n");
out.push_str(" rivet docs embed-syntax Full {{...}} syntax reference\n");
out
}

/// Show a specific topic.
pub fn show_topic(slug: &str, format: &str) -> String {
let Some(topic) = TOPICS.iter().find(|t| t.slug == slug) else {
Expand Down
120 changes: 119 additions & 1 deletion rivet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,25 @@ enum Command {
format: String,
},

/// Run an s-expression filter against the project and print matches
///
/// Mirror of the MCP `rivet_query` tool and the `{{query:(...)}}`
/// document embed — all three share `rivet_core::query::execute_sexpr`
/// so their results agree for the same filter.
Query {
/// The s-expression filter (e.g. '(and (= type "requirement") (has-tag "stpa"))')
#[arg(long)]
sexpr: String,

/// Maximum number of results (default: 100)
#[arg(long, default_value = "100")]
limit: usize,

/// Output format: "text" (default), "json", "ids"
#[arg(short, long, default_value = "text")]
format: String,
},

/// Stamp artifact(s) with AI provenance metadata
Stamp {
/// Artifact ID to stamp (or "all" for all artifacts in a file)
Expand Down Expand Up @@ -1214,6 +1233,11 @@ fn run(cli: Cli) -> Result<bool> {
Command::Remove { id, force } => cmd_remove(&cli, id, *force),
Command::Batch { file } => cmd_batch(&cli, file),
Command::Embed { query, format } => cmd_embed(&cli, query, format),
Command::Query {
sexpr,
limit,
format,
} => cmd_query(&cli, sexpr, *limit, format),
Command::Stamp {
id,
created_by,
Expand Down Expand Up @@ -5949,7 +5973,15 @@ fn cmd_docs(
} else if let Some(pattern) = grep {
print!("{}", docs::grep_docs(pattern, format, context));
} else if let Some(slug) = topic {
print!("{}", docs::show_topic(slug, format));
// Special meta-topic: `rivet docs embeds` lists every registered
// {{...}} token from `rivet_core::embed::EMBED_REGISTRY`. Kept in
// `docs` (rather than a new top-level subcommand) so it ships in
// the existing --help tree and stays near `docs embed-syntax`.
if slug == "embeds" {
print!("{}", docs::list_embeds(format));
} else {
print!("{}", docs::show_topic(slug, format));
}
} else {
print!("{}", docs::list_topics(format));
}
Expand Down Expand Up @@ -8757,6 +8789,92 @@ fn cmd_mcp(cli: &Cli) -> Result<bool> {
Ok(true)
}

/// `rivet query --sexpr "..."` — run an s-expression filter from the CLI.
///
/// Thin adapter over `rivet_core::query::execute_sexpr`, the same entry
/// point used by MCP's `rivet_query` tool and the `{{query:(...)}}` embed,
/// so all three surfaces emit the same match set for the same filter.
/// Three output formats: `text` (one line per match, id + title + status),
/// `json` (MCP-shape: `{filter, count, total, truncated, artifacts[]}`),
/// or `ids` (newline-separated IDs — handy for shell pipelines).
fn cmd_query(cli: &Cli, sexpr: &str, limit: usize, format: &str) -> Result<bool> {
validate_format(format, &["text", "json", "ids"])?;

let project = rivet_core::load_project_full(&cli.project)
.with_context(|| format!("loading project from {}", cli.project.display()))?;

let result =
rivet_core::query::execute_sexpr(&project.store, &project.graph, sexpr, Some(limit))
.map_err(|e| anyhow::anyhow!("{e}"))?;

match format {
"json" => {
let artifacts: Vec<serde_json::Value> = result
.matches
.iter()
.map(|a| {
let links: Vec<serde_json::Value> = a
.links
.iter()
.map(|l| {
serde_json::json!({"type": l.link_type, "target": l.target})
})
.collect();
serde_json::json!({
"id": a.id,
"type": a.artifact_type,
"title": a.title,
"status": a.status.as_deref().unwrap_or("-"),
"tags": a.tags,
"links": links,
"description": a.description.as_deref().unwrap_or(""),
})
})
.collect();
let out = serde_json::json!({
"filter": sexpr,
"count": artifacts.len(),
"total": result.total,
"truncated": result.truncated,
"artifacts": artifacts,
});
println!("{}", serde_json::to_string_pretty(&out)?);
}
"ids" => {
for a in &result.matches {
println!("{}", a.id);
}
}
_ => {
// text
if result.matches.is_empty() {
println!("No artifacts match: {sexpr}");
} else {
for a in &result.matches {
println!(
"{:16} {:16} {:8} {}",
a.id,
a.artifact_type,
a.status.as_deref().unwrap_or("-"),
a.title,
);
}
if result.truncated {
println!(
"\n{} result(s) shown, {} match total — raise --limit to see more.",
result.matches.len(),
result.total,
);
} else {
println!("\n{} result(s).", result.matches.len());
}
}
}
}

Ok(true)
}

fn cmd_lsp(cli: &Cli) -> Result<bool> {
use lsp_server::{Connection, Message, Response};
use lsp_types::*;
Expand Down
71 changes: 34 additions & 37 deletions rivet-cli/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,46 +943,43 @@ fn json_to_yaml_value(v: &Value) -> serde_yaml::Value {
// ── Query tool helper ─────────────────────────────────────────────────

fn tool_query(proj: &McpProject, params: &QueryParams) -> Result<Value> {
let expr = rivet_core::sexpr_eval::parse_filter(&params.filter).map_err(|errs| {
let msgs: Vec<String> = errs.iter().map(|e| e.to_string()).collect();
anyhow::anyhow!("invalid filter: {}", msgs.join("; "))
})?;

let limit = params.limit.unwrap_or(100);
let mut results: Vec<Value> = Vec::new();

for artifact in proj.store.iter() {
if !rivet_core::sexpr_eval::matches_filter_with_store(
&expr,
artifact,
&proj.graph,
&proj.store,
) {
continue;
}
let links_json: Vec<Value> = artifact
.links
.iter()
.map(|l| json!({"type": l.link_type, "target": l.target}))
.collect();
results.push(json!({
"id": artifact.id,
"type": artifact.artifact_type,
"title": artifact.title,
"status": artifact.status.as_deref().unwrap_or("-"),
"tags": artifact.tags,
"links": links_json,
"description": artifact.description.as_deref().unwrap_or(""),
}));
if results.len() >= limit {
break;
}
}
// Shared path with `rivet query` and the `{{query:...}}` embed so all
// three surfaces return the same artifact set for the same filter.
let result = rivet_core::query::execute_sexpr(
&proj.store,
&proj.graph,
&params.filter,
Some(params.limit.unwrap_or(100)),
)
.map_err(|e| anyhow::anyhow!("{e}"))?;

let artifacts: Vec<Value> = result
.matches
.iter()
.map(|a| {
let links_json: Vec<Value> = a
.links
.iter()
.map(|l| json!({"type": l.link_type, "target": l.target}))
.collect();
json!({
"id": a.id,
"type": a.artifact_type,
"title": a.title,
"status": a.status.as_deref().unwrap_or("-"),
"tags": a.tags,
"links": links_json,
"description": a.description.as_deref().unwrap_or(""),
})
})
.collect();

Ok(json!({
"filter": params.filter,
"count": results.len(),
"artifacts": results,
"count": artifacts.len(),
"total": result.total,
"truncated": result.truncated,
"artifacts": artifacts,
}))
}

Expand Down
49 changes: 49 additions & 0 deletions rivet-cli/src/render/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,58 @@ pub(crate) fn render_help(ctx: &RenderContext) -> String {
html.push_str("rivet schema list List artifact types\n");
html.push_str("rivet schema show TYPE Show type details\n");
html.push_str("rivet docs List documentation topics\n");
html.push_str("rivet docs embeds List {{...}} embed tokens\n");
html.push_str("rivet serve [-P PORT] Start dashboard\n");
html.push_str("</pre></div>");

// Registered embeds — sourced from rivet_core::embed::EMBED_REGISTRY so
// the dashboard listing matches `rivet docs embeds` exactly.
html.push_str(&render_embed_registry());

html
}

/// Render the embed registry table for the Help view.
///
/// Mirrors the output of `rivet docs embeds` so users can discover
/// `{{stats}}`, `{{query:(...)}}`, `{{artifact:ID}}`, etc. without having
/// to read the source or grep the docs.
fn render_embed_registry() -> String {
use rivet_core::embed::registry;
let specs = registry();

let mut html = String::with_capacity(4096);
html.push_str(
r#"<div class="card" style="padding:1.25rem;margin-top:1rem">
<h3 style="margin:0 0 1rem">Document Embeds</h3>
<p style="opacity:.7;font-size:.85rem;margin:0 0 .75rem">
Use <code>{{name[:args]}}</code> inside an artifact description or document body.
Run <code>rivet docs embeds</code> for the same list from the CLI, or
<code>rivet docs embed-syntax</code> for the full reference.
</p>
<table style="font-size:.85rem">
<thead><tr><th>Name</th><th>Args</th><th>Summary</th><th>Example</th></tr></thead>
<tbody>
"#,
);
for s in specs {
html.push_str(&format!(
"<tr><td><code>{name}</code>{marker}</td>\
<td><code>{args}</code></td>\
<td>{summary}</td>\
<td><code>{example}</code></td></tr>\n",
name = html_escape(s.name),
marker = if s.legacy {
r#" <span style="opacity:.6;font-size:.75rem">(inline)</span>"#
} else {
""
},
args = html_escape(s.args),
summary = html_escape(s.summary),
example = html_escape(s.example),
));
}
html.push_str("</tbody></table></div>");
html
}

Expand Down
Loading
Loading