Skip to content
Closed
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
11 changes: 11 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8502,3 +8502,14 @@ artifacts:
created-by: ai-assisted
model: claude-opus-5
timestamp: 2026-09-06T00:00:00Z
- id: REQ-328
type: requirement
title: "Bridge-schema coverage rules are reported but their definitions are not discoverable, so a failing rule cannot be acted on (#895)"
status: verified
description: "Reported from jess (#895). `rivet coverage` reports traceability rules that come from the embedded bridge schemas — the reporter saw constraint-has-requirement, controller-constraint-has-requirement and stpa-constraints-provide-evidence all at 0.0 percent — while `rivet docs schema/<bridge>` answered Unknown topic for every one of them. Reproduced exactly on current main: the reporter's own command returned 0 hits for all four spellings they tried, and `rivet docs schema/dev` served its schema normally by contrast. A rule you can watch fail and cannot look up is a rule you cannot act on, because nothing tells you which link type or target types would satisfy it — the same shape as a gate whose output a reader cannot turn into an action. The bridges were embedded all along as BRIDGE_SCHEMAS with filename, extends and content; they were simply never registered as documentation topics. Fixed by deriving the topics from that registry rather than listing them by hand, so a bridge added later cannot ship undocumented. Lookup, `docs --list` in both text and JSON, and `docs <slug>` all consult it. Deliberately NOT folded into topic_bodies, which feeds the embedded-doc invariants in `rivet docs check`: those scan markdown prose and bridge content is schema YAML, so it would be judged against rules written for something else. The oracle asserts over the registry rather than a fixed list — every entry must be both servable and present in the listing — so the next bridge cannot be added without docs. Negative-controlled: disabling the fallback reddens it with all seven bridges named."
release: v0.37.0
tags: [discoverability, schema, coverage]
provenance:
created-by: ai-assisted
model: claude-opus-5
timestamp: 2026-09-08T00:00:00Z
63 changes: 60 additions & 3 deletions rivet-cli/src/docs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
//! `rivet docs` — built-in searchable documentation.
//!
//! All documentation is embedded in the binary. Topics are searchable
Expand Down Expand Up @@ -371,6 +371,24 @@
///
/// Used by the subcommand-coverage gate to cross-reference clap subcommand
/// paths against documented topics.
/// Bridge schemas as doc topics, derived from the embedded registry (#895).
///
/// `rivet coverage` reports bridge traceability rules, so a user can watch one
/// fail; before this they could not look it up, and nothing told them which
/// link type or target types would satisfy it. Derived from
/// `BRIDGE_SCHEMAS` rather than listed by hand so a bridge added later cannot
/// ship undocumented.
///
/// Deliberately NOT folded into [`topic_bodies`]: that feeds the embedded-doc
/// invariants in `rivet docs check`, which scan markdown prose. Bridge content
/// is schema YAML and would be judged against rules written for prose.
fn bridge_topics() -> Vec<(String, &'static str)> {
rivet_core::embedded::BRIDGE_SCHEMAS
.iter()
.map(|b| (format!("schema/{}", b.filename), b.content))
.collect()
}

pub fn topic_slugs() -> Vec<&'static str> {
TOPICS.iter().map(|t| t.slug).collect()
}
Expand All @@ -388,7 +406,7 @@
/// True iff a topic with this slug is registered.
#[allow(dead_code)]
pub fn has_topic(slug: &str) -> bool {
TOPICS.iter().any(|t| t.slug == slug)
TOPICS.iter().any(|t| t.slug == slug) || bridge_topics().iter().any(|(s, _)| s == slug)
}

/// Return the raw content body of a topic, or `None` if no topic with
Expand All @@ -397,7 +415,16 @@
/// Used by the subcommand-coverage gate's umbrella rule to verify that a
/// parent topic actually mentions the child subcommand by name.
pub fn topic_content(slug: &str) -> Option<&'static str> {
TOPICS.iter().find(|t| t.slug == slug).map(|t| t.content)
TOPICS
.iter()
.find(|t| t.slug == slug)
.map(|t| t.content)
.or_else(|| {
bridge_topics()
.into_iter()
.find(|(s, _)| s == slug)
.map(|(_, c)| c)
})
}

// ── Embedded documentation ──────────────────────────────────────────────
Expand Down Expand Up @@ -1440,7 +1467,7 @@
/// List all available documentation topics.
pub fn list_topics(format: &str) -> String {
if format == "json" {
let items: Vec<serde_json::Value> = TOPICS
let mut items: Vec<serde_json::Value> = TOPICS
.iter()
.map(|t| {
serde_json::json!({
Expand All @@ -1450,6 +1477,13 @@
})
})
.collect();
items.extend(bridge_topics().into_iter().map(|(slug, _)| {
serde_json::json!({
"slug": slug,
"title": "Bridge schema",
"category": "Schemas",
})
}));
return serde_json::to_string_pretty(&serde_json::json!({
"command": "docs-list",
"topics": items,
Expand All @@ -1476,6 +1510,16 @@
out.push_str(" rivet docs <topic> Show a topic\n");
out.push_str(" rivet docs --grep <pat> Search across all docs\n");
out.push_str(" rivet docs <topic> -f json Machine-readable output\n");
// #895: bridge schemas are reported by `coverage` but were absent here,
// so nobody could find the topic even once it was servable.
let bridges = bridge_topics();
if !bridges.is_empty() {
out.push_str("\n Schemas (bridges)\n");
for (slug, _) in &bridges {
out.push_str(&format!(" {slug:<38} cross-domain traceability rules\n"));
}
}

out
}

Expand Down Expand Up @@ -1543,6 +1587,19 @@

/// Show a specific topic.
pub fn show_topic(slug: &str, format: &str) -> String {
if let Some((bslug, content)) = bridge_topics().into_iter().find(|(s, _)| s == slug) {
if format == "json" {
return serde_json::to_string_pretty(&serde_json::json!({
"command": "docs-show",
"topic": bslug,
"title": "Bridge schema",
"category": "Schemas",
"content": content,
}))
.unwrap_or_default();
}
return format!("# {bslug} — Bridge schema\n\n{content}");
}
let Some(topic) = TOPICS.iter().find(|t| t.slug == slug) else {
let mut out = format!("Unknown topic: {slug}\n\nAvailable topics:\n");
for t in TOPICS {
Expand Down
55 changes: 55 additions & 0 deletions rivet-cli/tests/cli_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10803,3 +10803,58 @@ fn add_without_id_still_derives_next_in_series() {
"derived id must continue the existing series"
);
}

/// Every embedded bridge schema must be discoverable through `rivet docs`
/// (#895, REQ-328).
///
/// `rivet coverage` reports bridge traceability rules — jess saw
/// `constraint-has-requirement` and two others — while `rivet docs
/// schema/<bridge>` answered "Unknown topic". A rule you can watch fail and
/// cannot look up is a rule you cannot act on: nothing tells you which link
/// type or target types would satisfy it.
///
/// Asserted over the registry rather than a hardcoded list, so a bridge added
/// later cannot ship without docs.
///
/// rivet: verifies REQ-328
#[test]
fn every_embedded_bridge_schema_is_documented() {
let bridges = rivet_core::embedded::BRIDGE_SCHEMAS;
assert!(
bridges.len() >= 5,
"expected the bridge registry to be populated, saw {}",
bridges.len()
);

let listed = Command::new(rivet_bin())
.args(["docs", "--list"])
.output()
.expect("docs --list");
let listed = String::from_utf8_lossy(&listed.stdout).to_string();

let mut missing_topic = Vec::new();
let mut missing_listing = Vec::new();
for b in bridges {
let slug = format!("schema/{}", b.filename);
let out = Command::new(rivet_bin())
.args(["docs", &slug])
.output()
.expect("docs <slug>");
let body = String::from_utf8_lossy(&out.stdout).to_string();
if body.contains("Unknown topic") || !out.status.success() {
missing_topic.push(slug.clone());
}
if !listed.contains(&slug) {
missing_listing.push(slug);
}
}
assert!(
missing_topic.is_empty(),
"these bridge schemas are reported by coverage but cannot be looked up: {missing_topic:?}"
);
assert!(
missing_listing.is_empty(),
"these bridge schemas are servable but absent from `docs --list`, so nobody \
will find them: {missing_listing:?}"
);
}
Loading