Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e362b6
fix(relay): name the column and order key each diagnostic is about
jeremi Aug 19, 2026
079f706
fix(relay): decode the SDMX series branch without a trailing refusal arm
jeremi Aug 19, 2026
b8e998a
feat(relay): render relayctl reports for adopters by default
jeremi Aug 19, 2026
08bea19
docs(site): assert what tutorial runs do, not how pages word them
jeremi Aug 19, 2026
e175eae
docs(site): report Solmara tutorial step counts instead of pinning them
jeremi Aug 19, 2026
74cf8bb
docs(site): write the style guide for readers, not runners
jeremi Aug 19, 2026
08cd78b
docs(site): organize the sidebar by what a reader came to do
jeremi Aug 19, 2026
1e5b145
docs(site): say what the install one-liner does and how to verify it
jeremi Aug 19, 2026
ee071a7
docs(site): correct the dev startup banner in four tutorials
jeremi Aug 19, 2026
35a0af7
docs(site): quote what Evidence tooling actually prints
jeremi Aug 19, 2026
5beb20e
docs(site): allow a link to the section the reader was sent to
jeremi Aug 19, 2026
b42f1e6
docs(site): state what a production signing key commits you to
jeremi Aug 19, 2026
a83078d
docs(site): give the configuration pages somewhere to go next
jeremi Aug 19, 2026
f834f23
docs(site): stop asking readers to copy a digest that cannot be theirs
jeremi Aug 19, 2026
e3e5def
docs(site): start the stack the compose tutorial configures
jeremi Aug 19, 2026
22d018d
test(docs): pin the tamper refusal to what the tool prints
jeremi Aug 19, 2026
9b49577
docs(site): name the section instead of pointing at it
jeremi Aug 19, 2026
56e31e4
docs(products): name the section instead of pointing at it
jeremi Aug 19, 2026
3a1890d
docs(site): keep demo identifiers off the command line
jeremi Aug 19, 2026
b941ff0
docs(site): fix sidebar carets, Rhai blocks, and Evidence citations
jeremi Aug 19, 2026
9e7ff09
docs(site): publish two tutorials and retire the duplicate chooser
jeremi Aug 19, 2026
440171d
docs(site): close four reader-facing gaps found in review
jeremi Aug 19, 2026
59fe6d2
docs(site): show relayctl's human output in the SQLite tutorial
jeremi Aug 19, 2026
708ccf8
docs(site): correct relayctl report output description
jeremi Aug 19, 2026
0814352
chore(identifiers): regenerate the catalog for the edited concept source
jeremi Aug 19, 2026
85ccf9e
docs(spec): specify the readable relayctl default output
jeremi Aug 19, 2026
63750a9
docs(site): stop printing values the reader cannot reproduce
jeremi Aug 19, 2026
3a60f61
fix(relay): neutralize hostile schema text in relayctl reports
jeremi Aug 19, 2026
5c9cdfd
fix(relay): point column diagnostics at an authored key path
jeremi Aug 19, 2026
35961f6
fix(relay): escape every report line at the rendering boundary
jeremi Aug 19, 2026
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
351 changes: 313 additions & 38 deletions crates/registry-relay-v2/src/compiler.rs

Large diffs are not rendered by default.

26 changes: 23 additions & 3 deletions crates/registry-relay-v2/src/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,8 @@ fn sdmx_json_rows(document: &Value) -> Option<Vec<BTreeMap<String, Value>>> {
sdmx_decode_values(&mut row, values, &measures, &attributes)?;
rows.push(row);
}
} else if let Some(series) = data_set.get("series").and_then(Value::as_object) {
} else {
let series = data_set.get("series").and_then(Value::as_object)?;
for (series_key, series_document) in series {
let series_values = sdmx_decode_key(series_key, &series_dimensions)?;
let observations = series_document.get("observations")?.as_object()?;
Expand All @@ -1100,8 +1101,6 @@ fn sdmx_json_rows(document: &Value) -> Option<Vec<BTreeMap<String, Value>>> {
rows.push(row);
}
}
} else {
return None;
}
Some(rows)
}
Expand Down Expand Up @@ -1704,6 +1703,27 @@ dataflow,EXAMPLE:RATES(1.0.0),R,EX-A,2024-Q1,65.5,PERCENT\n";
);
}

#[test]
fn sdmx_json_rows_refuse_a_data_set_without_observations_or_series() {
let structures = json!([{
"dimensions": {
"series": [{"id": "REF_AREA", "values": [{"id": "EX-A"}]}],
"observation": [{"id": "TIME_PERIOD", "values": [{"value": "2024-Q1"}]}]
},
"measures": {"observation": [{"id": "OBS_VALUE"}]},
"attributes": {"observation": [{
"id": "UNIT_MEASURE", "values": [{"id": "PERCENT"}]
}]}
}]);

let neither = json!({"data": {"dataSets": [{}], "structures": structures}});
assert_eq!(sdmx_json_rows(&neither), None);

let unusable_series =
json!({"data": {"dataSets": [{"series": []}], "structures": structures}});
assert_eq!(sdmx_json_rows(&unusable_series), None);
}

#[test]
fn fixture_tokens_have_a_bounded_jwt_shape_without_exposing_claims() {
let token = fixture_token("fixture-a");
Expand Down
254 changes: 193 additions & 61 deletions crates/registry-relayctl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ use std::io::{self, Write};
use std::process::ExitCode;

use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use serde::Serialize;

mod report;
Comment thread
jeremi marked this conversation as resolved.
mod shared;
mod tooling_editor;

use crate::shared::ToolingReport;

const DOMAIN_REFUSAL_EXIT: u8 = 1;
const USAGE_EXIT: u8 = 2;
const OPERATIONAL_FAILURE_EXIT: u8 = 3;
Expand Down Expand Up @@ -212,7 +214,6 @@ where
command => command,
};

let command_name = command.name();
let report = match shared::execute(command) {
Ok(report) => report,
Err(error) => {
Expand All @@ -221,7 +222,7 @@ where
}
};

if render_report(command_name, &report, json, stdout).is_err() {
if render_report(&report, json, stdout).is_err() {
let _ = writeln!(stderr, "relayctl: output could not be written");
return ExitCode::from(OPERATIONAL_FAILURE_EXIT);
}
Expand All @@ -233,21 +234,6 @@ where
}
}

impl Command {
fn name(&self) -> &'static str {
match self {
Self::Init(_) => "init",
Self::Inspect(_) => "inspect",
Self::Check(_) => "check",
Self::Generate(_) => "generate",
Self::Test(_) => "test",
Self::Diff(_) => "diff",
Self::Package(_) => "package",
Self::Tooling(_) => "tooling",
}
}
}

fn run_tooling(
command: ToolingCommand,
json: bool,
Expand Down Expand Up @@ -297,21 +283,17 @@ fn run_tooling(
}
}

fn render_report<T: Serialize>(
command: &str,
report: &T,
json: bool,
output: &mut dyn Write,
) -> io::Result<()> {
/// Write one shared report: the machine document under `--json`, and the plain
/// adopter rendering otherwise. The shared report is the sole source of command
/// details in both. Rendering it does not reinterpret compiler outcomes or
/// change classes, and the JSON document is the same one either way.
fn render_report(report: &ToolingReport, json: bool, output: &mut dyn Write) -> io::Result<()> {
if json {
serde_json::to_writer_pretty(&mut *output, report).map_err(io::Error::other)?;
writeln!(output)
} else {
writeln!(output, "relayctl {command}")?;
// The shared report is the sole source of command details. Rendering
// it here does not reinterpret compiler outcomes or change classes.
serde_json::to_writer_pretty(&mut *output, report).map_err(io::Error::other)?;
writeln!(output)
let rendered = report::render_human(report).map_err(io::Error::other)?;
output.write_all(rendered.as_bytes())
Comment thread
jeremi marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -545,28 +527,188 @@ mod tests {
.starts_with(".relay-v2-editor-transaction-")));
}

/// One shared report of every kind the tooling facade can return, as the
/// exact JSON document `--json` has always written.
const REPORT_DOCUMENTS: [&str; 7] = [
concat!(
"{\n",
" \"status\": \"success\",\n",
" \"diagnostics\": [],\n",
" \"details\": {\n",
" \"kind\": \"initialized\",\n",
" \"files\": [\n",
" \"registry.yaml\"\n",
" ]\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"success\",\n",
" \"diagnostics\": [],\n",
" \"details\": {\n",
" \"kind\": \"schema-inspection\",\n",
" \"fingerprint\": \"sha256:aaaa\",\n",
" \"objects\": [\n",
" {\n",
" \"kind\": \"table\",\n",
" \"name\": \"source_records\",\n",
" \"tableName\": \"source_records\",\n",
" \"columns\": [\n",
" {\n",
" \"name\": \"record_identifier\",\n",
" \"declaredType\": \"TEXT\",\n",
" \"nullable\": false,\n",
" \"primaryKey\": true\n",
" }\n",
" ]\n",
" }\n",
" ],\n",
" \"starter_file\": null\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"refused\",\n",
" \"diagnostics\": [\n",
" {\n",
" \"severity\": \"error\",\n",
" \"code\": \"runtime.issuer_missing\",\n",
" \"location\": \"runtime.yaml.authentication.issuer\",\n",
" \"message\": \"a Registry with protected operations requires one configured issuer\"\n",
" }\n",
" ],\n",
" \"details\": {\n",
" \"kind\": \"check\",\n",
" \"contract_revision\": null,\n",
" \"production\": true,\n",
" \"configuration_key_paths\": null\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"success\",\n",
" \"diagnostics\": [],\n",
" \"details\": {\n",
" \"kind\": \"generate\",\n",
" \"contract_revision\": \"sha256:bbbb\",\n",
" \"artifacts\": [\n",
" {\n",
" \"id\": \"capability-inventory\",\n",
" \"path\": \"artifacts/capabilities.json\",\n",
" \"sha256\": \"sha256:cccc\"\n",
" }\n",
" ]\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"success\",\n",
" \"diagnostics\": [],\n",
" \"details\": {\n",
" \"kind\": \"test\",\n",
" \"contract_revision\": \"sha256:dddd\",\n",
" \"report\": {\n",
" \"registryIdentifier\": \"urn:example:registry:records\",\n",
" \"selectedFixture\": null,\n",
" \"steps\": [\n",
" {\n",
" \"id\": \"first-page\",\n",
" \"operationIdentifier\": \"record.list\",\n",
" \"expectedStatus\": 200,\n",
" \"actualStatus\": 200,\n",
" \"actualCode\": null,\n",
" \"passed\": true\n",
" }\n",
" ],\n",
" \"diagnostics\": []\n",
" }\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"success\",\n",
" \"diagnostics\": [],\n",
" \"details\": {\n",
" \"kind\": \"diff\",\n",
" \"report\": {\n",
" \"previousRevision\": \"sha256:eeee\",\n",
" \"currentRevision\": \"sha256:ffff\",\n",
" \"changes\": [\n",
" {\n",
" \"class\": \"filter-removed\",\n",
" \"impact\": \"breaking\",\n",
" \"location\": \"resources[0].operations.list.filters\",\n",
" \"description\": \"a request filter was removed\"\n",
" }\n",
" ]\n",
" }\n",
" }\n",
"}"
),
concat!(
"{\n",
" \"status\": \"refused\",\n",
" \"diagnostics\": [\n",
" {\n",
" \"severity\": \"error\",\n",
" \"code\": \"classification.unreviewed\",\n",
" \"location\": \"resources[0].sourceColumnClassifications\",\n",
" \"message\": \"production compilation requires reviewed classification\"\n",
" }\n",
" ],\n",
" \"details\": {\n",
" \"kind\": \"package\",\n",
" \"manifest\": null\n",
" }\n",
"}"
),
];

fn parsed_report(document: &str) -> ToolingReport {
serde_json::from_str(document).expect("the report document parses")
}

fn written(report: &ToolingReport, json: bool) -> String {
let mut output = Vec::new();
render_report(report, json, &mut output).expect("report renders");
String::from_utf8(output).expect("output is UTF-8")
}

#[test]
fn json_reports_are_one_valid_document() {
#[derive(Serialize)]
struct Report<'a> {
status: &'a str,
summary: &'a str,
let output = written(&parsed_report(REPORT_DOCUMENTS[1]), true);

let value: serde_json::Value = serde_json::from_str(&output).expect("valid JSON");
assert_eq!(value["status"], "success");
assert_eq!(value["details"]["kind"], "schema-inspection");
}

#[test]
fn json_output_stays_byte_identical_to_the_shared_report_document() {
for document in REPORT_DOCUMENTS {
let report = parsed_report(document);

assert_eq!(written(&report, true), format!("{document}\n"));
}
}

let mut output = Vec::new();
render_report(
"inspect",
&Report {
status: "accepted",
summary: "schema structure inspected",
},
true,
&mut output,
)
.expect("report renders");
#[test]
fn the_default_output_is_the_plain_rendering_and_never_the_document() {
for document in REPORT_DOCUMENTS {
let report = parsed_report(document);

let output = written(&report, false);

let value: serde_json::Value = serde_json::from_slice(&output).expect("valid JSON");
assert_eq!(value["status"], "accepted");
assert_eq!(output, report::render_human(&report).expect("renders"));
assert!(!output.starts_with('{'), "default output opened a document");
assert!(!output.contains("\"status\""), "default output kept JSON");
assert!(output.ends_with('\n'));
}
}

#[test]
Expand All @@ -581,23 +723,13 @@ mod tests {

#[test]
fn json_rendering_is_deterministic_and_has_one_trailing_newline() {
#[derive(Serialize)]
struct Report<'a> {
status: &'a str,
summary: &'a str,
}
let report = parsed_report(REPORT_DOCUMENTS[1]);

let mut first = Vec::new();
let mut second = Vec::new();
let report = Report {
status: "accepted",
summary: "schema structure inspected",
};
render_report("inspect", &report, true, &mut first).expect("report renders");
render_report("inspect", &report, true, &mut second).expect("report repeats");
let first = written(&report, true);
let second = written(&report, true);

assert_eq!(first, second);
assert!(first.ends_with(b"\n"));
assert!(!first.ends_with(b"\n\n"));
assert!(first.ends_with('\n'));
assert!(!first.ends_with("\n\n"));
}
}
Loading