mod document;
mod fidelity;
mod graph;
mod item;
mod join;
mod output;
mod schema;
mod schema_infer;
use std::collections::BTreeMap;
use std::fmt;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use cadmpeg_core::dialect::{Admission, DialectLayers, DialectMatch};
use cadmpeg_ir::SourceMeta;
use clap::{Args, Subcommand};
use serde::de::{IgnoredAny, SeqAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use fidelity::FidelityArgs;
use graph::GraphArgs;
use item::ItemArgs;
use join::JoinArgs;
use schema::SchemaArgs;
#[derive(Debug, Subcommand)]
pub(crate) enum QueryView {
#[command(flatten)]
Aggregate(AggregateView),
#[command(visible_alias = "record", alias = "get")]
Item(ItemArgs),
Schema(SchemaArgs),
Graph(GraphArgs),
Join(JoinArgs),
Fidelity(FidelityArgs),
}
#[derive(Debug, Subcommand)]
pub(crate) enum AggregateView {
Summary(QueryArgs),
Coverage(QueryArgs),
Findings(QueryArgs),
Losses(QueryArgs),
#[command(visible_alias = "arenas")]
Counts(QueryArgs),
}
#[derive(Debug, Args)]
pub(crate) struct QueryArgs {
file: PathBuf,
#[arg(long)]
json: bool,
}
impl AggregateView {
fn args(&self) -> &QueryArgs {
match self {
Self::Summary(args)
| Self::Coverage(args)
| Self::Findings(args)
| Self::Losses(args)
| Self::Counts(args) => args,
}
}
}
enum Artifact {
Report(Box<ReportProbe>),
Cadir(CadirProbe),
Sidecar(SidecarProbe),
}
impl Artifact {
fn kind_name(&self) -> &'static str {
match self {
Self::Report(_) => "command report",
Self::Cadir(_) => "CADIR document",
Self::Sidecar(_) => "decode sidecar",
}
}
}
#[derive(Deserialize)]
struct KindProbe {
command: Option<String>,
status: Option<String>,
ir_version: Option<Value>,
ir_sha256: Option<String>,
}
#[derive(Deserialize)]
struct ReportProbe {
command: String,
#[serde(default)]
generator: Option<String>,
status: String,
#[serde(default)]
refusal: Option<RefusalProbe>,
#[serde(default)]
summary: Option<ContainerSummaryProbe>,
#[serde(default)]
decode_report: Option<DecodeReportProbe>,
#[serde(default)]
check_report: Option<CheckReportProbe>,
#[serde(default)]
export: Option<ExportReportProbe>,
#[serde(default)]
measure: Option<MeasureProbe>,
}
#[derive(Deserialize)]
struct MeasureProbe {
#[serde(default)]
bodies: Vec<serde::de::IgnoredAny>,
#[serde(default)]
faces: Vec<serde::de::IgnoredAny>,
#[serde(default)]
edges: Vec<serde::de::IgnoredAny>,
#[serde(default)]
surfaces: Vec<serde::de::IgnoredAny>,
#[serde(default, deserialize_with = "deserialize_measure_document")]
document: Option<MeasureDocumentProbe>,
}
#[derive(Deserialize)]
struct MeasureDocumentProbe {
#[serde(default)]
unavailable: BTreeMap<String, BTreeMap<String, u64>>,
#[serde(default, deserialize_with = "deserialize_measure_work")]
work: Option<MeasureWorkProbe>,
}
#[derive(Deserialize)]
struct MeasureWorkProbe {
#[serde(default, deserialize_with = "deserialize_work_allowance")]
allowance: Option<u64>,
#[serde(default, deserialize_with = "deserialize_work_used")]
used: Option<u64>,
#[serde(default, deserialize_with = "deserialize_work_exhausted")]
exhausted: Option<bool>,
}
cadmpeg_core::named_optional_field!(
deserialize_measure_document,
MeasureDocumentProbe,
"document"
);
cadmpeg_core::named_optional_field!(deserialize_measure_work, MeasureWorkProbe, "work");
cadmpeg_core::named_optional_field!(deserialize_work_allowance, u64, "allowance");
cadmpeg_core::named_optional_field!(deserialize_work_used, u64, "used");
cadmpeg_core::named_optional_field!(deserialize_work_exhausted, bool, "exhausted");
#[derive(Deserialize)]
struct RefusalProbe {
#[serde(default)]
stage: Option<String>,
#[serde(default)]
code: Option<String>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
dialects: Option<DialectLayers>,
#[serde(default)]
target: Option<Value>,
}
#[derive(Deserialize)]
struct ContainerSummaryProbe {
#[serde(default)]
format: Option<String>,
#[serde(default)]
dialects: Option<DialectLayers>,
#[serde(default)]
losses: Vec<LossProbe>,
}
#[derive(Deserialize)]
struct ExportReportProbe {
#[serde(default)]
identity: Option<ExportIdentityProbe>,
#[serde(default)]
losses: Vec<LossProbe>,
}
#[derive(Deserialize)]
#[serde(tag = "payload", rename_all = "snake_case")]
enum ExportIdentityProbe {
Cadir {},
Native { target: String },
}
#[derive(Deserialize)]
struct DecodeReportProbe {
#[serde(default)]
format: Option<String>,
#[serde(default)]
transfer: Option<DecodeTransferProbe>,
#[serde(default)]
coverage: BTreeMap<String, u64>,
#[serde(default)]
losses: Vec<LossProbe>,
#[serde(default)]
dialects: Option<DialectLayers>,
}
#[derive(Deserialize)]
struct DecodeTransferProbe {
#[serde(default)]
transfer: Option<String>,
#[serde(default)]
geometry_transferred: Option<bool>,
}
#[derive(Deserialize)]
struct CheckReportProbe {
#[serde(default)]
entity_counts: BTreeMap<String, u64>,
#[serde(default)]
findings: Vec<FindingProbe>,
#[serde(default)]
losses: Vec<LossProbe>,
}
#[derive(Deserialize)]
struct FindingProbe {
#[serde(default)]
check: Option<String>,
#[serde(default)]
severity: Option<String>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
entity: Option<String>,
}
#[derive(Deserialize)]
struct LossProbe {
#[serde(default)]
code: Option<LossCodeProbe>,
#[serde(default)]
severity: Option<String>,
#[serde(default)]
message: Option<String>,
}
#[derive(Deserialize)]
struct LossCodeProbe {
namespace: String,
code: String,
}
impl LossCodeProbe {
fn display(&self) -> String {
format!("{}/{}", self.namespace, self.code)
}
}
#[cfg(test)]
mod tests {
use super::{push_decode_dialect_summary, DecodeReportProbe};
#[test]
fn summary_cells_keep_text_and_json_distinct_from_field_names() {
let text = super::SummaryCell::from("not JSON".to_owned());
let structured = super::SummaryCell::from(
serde_json::value::to_raw_value(&serde_json::json!({"a": 1})).unwrap(),
);
let rows = std::collections::BTreeMap::from([
("ordinary_dialects", text),
("other_name", structured),
]);
let value = serde_json::to_value(&rows).unwrap();
assert_eq!(value["ordinary_dialects"], "not JSON");
assert_eq!(value["other_name"], serde_json::json!({"a": 1}));
}
#[test]
fn decode_summary_names_every_extra_dialect_layer() {
let decode: DecodeReportProbe = serde_json::from_value(serde_json::json!({
"dialects": {
"primary": {
"dialect": "f3d:archive-2",
"declared": {"manifest_version": "2"},
"admission": "admitted"
},
"extra": [{
"dialect": "acis:sab-22300",
"declared": {"save_format": "22300"},
"admission": {"unverified": {"using": "sab-22200"}},
"instance": "member:model.sab"
}]
}
}))
.unwrap();
let mut rows = Vec::new();
push_decode_dialect_summary(&mut rows, &decode).unwrap();
let rows = rows
.into_iter()
.map(|(key, value)| (key, value.as_text().to_owned()))
.collect::<Vec<_>>();
assert_eq!(
rows,
[
("decode_dialect_layers".to_owned(), "2".to_owned()),
(
"decode_dialects".to_owned(),
r#"{"primary":{"format":"f3d","dialect":"f3d:archive-2","declared":{"manifest_version":"2"},"admission":"admitted"},"extra":[{"format":"acis","dialect":"acis:sab-22300","declared":{"save_format":"22300"},"admission":{"unverified":{"using":"acis:sab-22200"}},"instance":"member:model.sab"}]}"#.to_owned()
),
("decode_dialect_format".to_owned(), "f3d".to_owned()),
("decode_dialect".to_owned(), "f3d:archive-2".to_owned()),
("decode_dialect_admission".to_owned(), "admitted".to_owned()),
(
"decode_dialect_declared".to_owned(),
r#"{"manifest_version":"2"}"#.to_owned()
),
]
);
}
}
#[derive(Deserialize)]
struct CadirProbe {
ir_version: String,
#[serde(default)]
source: Option<SourceMeta>,
#[serde(default)]
model: BTreeMap<String, ArenaLen>,
#[serde(default)]
native: BTreeMap<String, BTreeMap<String, ArenaLen>>,
}
#[derive(Deserialize)]
struct SidecarProbe {
#[serde(default)]
report: Option<DecodeReportProbe>,
}
struct ArenaLen(u64);
impl<'de> Deserialize<'de> for ArenaLen {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct LenVisitor;
impl<'de> Visitor<'de> for LenVisitor {
type Value = ArenaLen;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an arena array")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<ArenaLen, A::Error> {
let mut len = 0;
while seq.next_element::<IgnoredAny>()?.is_some() {
len += 1;
}
Ok(ArenaLen(len))
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<ArenaLen, A::Error> {
while map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
Ok(ArenaLen(0))
}
fn visit_unit<E>(self) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
fn visit_bool<E>(self, _: bool) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
fn visit_i64<E>(self, _: i64) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
fn visit_u64<E>(self, _: u64) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
fn visit_f64<E>(self, _: f64) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
fn visit_str<E>(self, _: &str) -> Result<ArenaLen, E> {
Ok(ArenaLen(0))
}
}
deserializer.deserialize_any(LenVisitor)
}
}
pub(crate) fn run(view: &QueryView) -> Result<()> {
match view {
QueryView::Aggregate(view) => run_aggregate(view),
QueryView::Item(args) => item::run(args),
QueryView::Schema(args) => schema::run(args),
QueryView::Graph(args) => graph::run(args),
QueryView::Join(args) => join::run(args),
QueryView::Fidelity(args) => fidelity::run(&args.file, args.mode()?),
}
}
fn run_aggregate(view: &AggregateView) -> Result<()> {
let args = view.args();
let bytes = read_input(&args.file)?;
let artifact = detect(&bytes, &args.file)?;
match view {
AggregateView::Summary(args) => summary(&artifact, args),
AggregateView::Coverage(args) => coverage(&artifact, args),
AggregateView::Findings(args) => findings(&artifact, args),
AggregateView::Losses(args) => losses(&artifact, args),
AggregateView::Counts(args) => counts(&artifact, args),
}
}
const MAX_QUERY_INPUT_BYTES: u64 = 256 * 1024 * 1024;
fn read_input(path: &Path) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
if path == Path::new("-") {
std::io::copy(
&mut std::io::stdin().lock().take(MAX_QUERY_INPUT_BYTES + 1),
&mut bytes,
)
.context("reading standard input")?;
} else {
let file =
std::fs::File::open(path).with_context(|| format!("reading {}", path.display()))?;
if file.metadata()?.len() > MAX_QUERY_INPUT_BYTES {
bail!(
"{} exceeds the query input limit of 256 MiB",
path.display()
);
}
std::io::copy(&mut file.take(MAX_QUERY_INPUT_BYTES + 1), &mut bytes)
.with_context(|| format!("reading {}", path.display()))?;
}
if u64::try_from(bytes.len())? > MAX_QUERY_INPUT_BYTES {
bail!(
"{} exceeds the query input limit of 256 MiB",
path.display()
);
}
Ok(bytes)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ArtifactKind {
Report,
Cadir,
Sidecar,
}
fn sniff_kind(bytes: &[u8], path: &Path) -> Result<ArtifactKind> {
let sniff: KindProbe = serde_json::from_slice(bytes).with_context(|| {
format!(
"{} is not a JSON object; query reads a command report (--report/-o), \
a decoded CADIR document, or a .fidelity.json decode sidecar",
path.display()
)
})?;
let kind = if sniff.command.is_some() && sniff.status.is_some() {
ArtifactKind::Report
} else if sniff.ir_sha256.is_some() {
ArtifactKind::Sidecar
} else if sniff.ir_version.is_some() {
ArtifactKind::Cadir
} else {
bail!(
"{} is JSON but not a recognized artifact; query reads a command report \
(top-level `command` and `status`), a decoded CADIR document \
(`ir_version` and `model`), or a .fidelity.json decode sidecar (`ir_sha256`)",
path.display()
)
};
let found = sniff.ir_version.as_ref().and_then(Value::as_str);
if found != Some(cadmpeg_ir::IR_VERSION) {
bail!(
"{} has ir_version {}; this build reads ir_version {}",
path.display(),
found.unwrap_or("<absent or non-string>"),
cadmpeg_ir::IR_VERSION
);
}
Ok(kind)
}
fn detect(bytes: &[u8], path: &Path) -> Result<Artifact> {
match sniff_kind(bytes, path)? {
ArtifactKind::Report => {
let report: ReportProbe = serde_json::from_slice(bytes)
.with_context(|| format!("parsing the command report {}", path.display()))?;
Ok(Artifact::Report(Box::new(report)))
}
ArtifactKind::Cadir => {
let cadir: CadirProbe = serde_json::from_slice(bytes)
.with_context(|| format!("parsing the CADIR document {}", path.display()))?;
Ok(Artifact::Cadir(cadir))
}
ArtifactKind::Sidecar => {
let sidecar: SidecarProbe = serde_json::from_slice(bytes)
.with_context(|| format!("parsing the decode sidecar {}", path.display()))?;
Ok(Artifact::Sidecar(sidecar))
}
}
}
fn cell(text: &str) -> String {
text.replace(['\t', '\n', '\r'], " ")
}
fn opt(text: Option<&String>) -> String {
text.map(|value| cell(value)).unwrap_or_default()
}
fn print_json(view: &str, payload: impl Serialize) -> Result<()> {
#[derive(Serialize)]
struct Body<'a, T> {
payload: T,
view: &'a str,
}
impl<T: Serialize> crate::commands::reporting::ReportBody for Body<'_, T> {}
println!(
"{}",
crate::commands::reporting::command_report_json("query", Body { payload, view })?
);
Ok(())
}
#[derive(Serialize)]
#[serde(untagged)]
enum SummaryCell {
Text(String),
Structured(Box<serde_json::value::RawValue>),
}
impl From<String> for SummaryCell {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<Box<serde_json::value::RawValue>> for SummaryCell {
fn from(value: Box<serde_json::value::RawValue>) -> Self {
Self::Structured(value)
}
}
impl SummaryCell {
fn as_text(&self) -> &str {
match self {
Self::Text(text) => text,
Self::Structured(value) => value.get(),
}
}
}
fn counts_json(map: &BTreeMap<String, u64>) -> serde_json::Value {
serde_json::json!(map)
}
fn summary(artifact: &Artifact, args: &QueryArgs) -> Result<()> {
let mut rows: Vec<(String, SummaryCell)> =
vec![("kind".to_owned(), artifact.kind_name().to_owned().into())];
match artifact {
Artifact::Report(report) => {
rows.push(("command".to_owned(), cell(&report.command).into()));
rows.push(("status".to_owned(), cell(&report.status).into()));
if let Some(refusal) = &report.refusal {
if let Some(stage) = &refusal.stage {
rows.push(("refusal_stage".to_owned(), cell(stage).into()));
}
if let Some(code) = &refusal.code {
rows.push(("refusal_code".to_owned(), cell(code).into()));
}
if let Some(message) = &refusal.message {
rows.push(("refusal_message".to_owned(), cell(message).into()));
}
push_dialect_layers_summary(&mut rows, "refusal", refusal.dialects.as_ref())?;
if let Some(target) = &refusal.target {
rows.push((
"refusal_target".to_owned(),
serde_json::value::to_raw_value(target)?.into(),
));
}
}
if let Some(generator) = &report.generator {
rows.push(("generator".to_owned(), cell(generator).into()));
}
if let Some(summary) = &report.summary {
if let Some(format) = &summary.format {
rows.push(("inspect_format".to_owned(), cell(format).into()));
}
push_dialect_layers_summary(&mut rows, "inspect", summary.dialects.as_ref())?;
}
match &report.decode_report {
Some(decode) => {
if let Some(format) = &decode.format {
rows.push(("decode_format".to_owned(), cell(format).into()));
}
if let Some(transfer) = decode
.transfer
.as_ref()
.and_then(|state| state.transfer.as_ref())
{
rows.push(("decode_transfer".to_owned(), cell(transfer).into()));
}
if let Some(geometry) = decode
.transfer
.as_ref()
.and_then(|state| state.geometry_transferred)
{
rows.push((
"geometry_transferred".to_owned(),
geometry.to_string().into(),
));
}
rows.push((
"coverage_rows".to_owned(),
decode.coverage.len().to_string().into(),
));
rows.push((
"decode_losses".to_owned(),
decode.losses.len().to_string().into(),
));
push_decode_dialect_summary(&mut rows, decode)?;
}
None => rows.push(("decode_report".to_owned(), "null".to_owned().into())),
}
match &report.check_report {
Some(validation) => {
rows.push((
"findings".to_owned(),
format!(
"{} ({} error, {} warning)",
validation.findings.len(),
count_severity(&validation.findings, &["error", "blocking"]),
count_severity(&validation.findings, &["warning"]),
)
.into(),
));
rows.push((
"check_losses".to_owned(),
validation.losses.len().to_string().into(),
));
rows.push((
"entity_count_rows".to_owned(),
validation.entity_counts.len().to_string().into(),
));
}
None => rows.push(("check_report".to_owned(), "null".to_owned().into())),
}
if let Some(measure) = &report.measure {
push_measure_summary(&mut rows, measure);
}
if let Some(export) = &report.export {
let (payload, target) = match &export.identity {
Some(ExportIdentityProbe::Cadir {}) => (Some("cadir"), None),
Some(ExportIdentityProbe::Native { target }) => {
(Some("native"), Some(target.as_str()))
}
None => (None, None),
};
if let Some(payload) = payload {
rows.push(("export_payload".to_owned(), cell(payload).into()));
}
rows.push((
"export_target".to_owned(),
target.map_or_else(|| "null".to_owned(), cell).into(),
));
rows.push((
"export_losses".to_owned(),
export.losses.len().to_string().into(),
));
}
}
Artifact::Cadir(cadir) => {
rows.push(("ir_version".to_owned(), cell(&cadir.ir_version).into()));
match &cadir.source {
Some(source) => {
rows.push(("source_format".to_owned(), cell(source.format()).into()));
push_dialect_layers_summary(&mut rows, "source", source.dialects())?;
}
None => rows.push(("source".to_owned(), "null".to_owned().into())),
}
rows.push((
"model_arenas".to_owned(),
cadir.model.len().to_string().into(),
));
rows.push((
"model_entities".to_owned(),
cadir
.model
.values()
.map(|len| len.0)
.sum::<u64>()
.to_string()
.into(),
));
for (namespace, arenas) in &cadir.native {
rows.push((
format!("native.{namespace}.arenas"),
arenas.len().to_string().into(),
));
rows.push((
format!("native.{namespace}.entities"),
arenas
.values()
.map(|len| len.0)
.sum::<u64>()
.to_string()
.into(),
));
}
}
Artifact::Sidecar(sidecar) => {
rows.push((
"sidecar_fidelity_validation".to_owned(),
"not_run".to_owned().into(),
));
match &sidecar.report {
Some(decode) => {
if let Some(format) = &decode.format {
rows.push(("decode_format".to_owned(), cell(format).into()));
}
rows.push((
"coverage_rows".to_owned(),
decode.coverage.len().to_string().into(),
));
rows.push((
"decode_losses".to_owned(),
decode.losses.len().to_string().into(),
));
push_decode_dialect_summary(&mut rows, decode)?;
}
None => rows.push(("report".to_owned(), "null".to_owned().into())),
}
}
}
if args.json {
let map = rows.into_iter().collect::<BTreeMap<_, _>>();
print_json("summary", map)?;
} else {
println!("field\tvalue");
for (field, value) in rows {
println!("{field}\t{}", value.as_text());
}
}
Ok(())
}
const UNDEFINED_CATEGORY: &str = "undefined";
fn push_measure_summary(rows: &mut Vec<(String, SummaryCell)>, measure: &MeasureProbe) {
for (level, count) in [
("bodies", measure.bodies.len()),
("faces", measure.faces.len()),
("edges", measure.edges.len()),
("surfaces", measure.surfaces.len()),
] {
rows.push((format!("measure_{level}"), count.to_string().into()));
}
let Some(document) = &measure.document else {
return;
};
let mut undefined = 0_u64;
let mut per_category: Vec<(&String, u64)> = Vec::new();
for (category, codes) in &document.unavailable {
let count = codes.values().sum::<u64>();
if category.as_str() == UNDEFINED_CATEGORY {
undefined += count;
} else {
per_category.push((category, count));
}
}
rows.push((
"measure_unavailable".to_owned(),
per_category
.iter()
.map(|(_, count)| count)
.sum::<u64>()
.to_string()
.into(),
));
for (category, count) in per_category {
rows.push((
format!("measure_unavailable.{}", cell(category)),
count.to_string().into(),
));
}
rows.push(("measure_undefined".to_owned(), undefined.to_string().into()));
if let Some(work) = &document.work {
if let (Some(used), Some(allowance)) = (work.used, work.allowance) {
rows.push((
"measure_work".to_owned(),
format!("{used} of {allowance}").into(),
));
}
if let Some(exhausted) = work.exhausted {
rows.push((
"measure_work_exhausted".to_owned(),
exhausted.to_string().into(),
));
}
}
}
fn push_decode_dialect_summary(
rows: &mut Vec<(String, SummaryCell)>,
decode: &DecodeReportProbe,
) -> Result<()> {
push_dialect_layers_summary(rows, "decode", decode.dialects.as_ref())
}
struct DialectLayersOutput<'a>(&'a DialectLayers);
impl Serialize for DialectLayersOutput<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("DialectLayers", 2)?;
state.serialize_field("primary", &DialectMatchOutput(self.0.primary()))?;
let extra = self
.0
.iter()
.skip(1)
.map(DialectMatchOutput)
.collect::<Vec<_>>();
state.serialize_field("extra", &extra)?;
state.end()
}
}
struct DialectMatchOutput<'a>(&'a DialectMatch);
impl Serialize for DialectMatchOutput<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let matched = self.0;
let mut state = serializer.serialize_struct("DialectMatch", 5)?;
state.serialize_field("format", matched.format())?;
state.serialize_field("dialect", matched.dialect())?;
if !matched.declared().is_empty() {
state.serialize_field("declared", matched.declared())?;
}
let admission = admission_value(matched);
state.serialize_field("admission", &admission)?;
if let Some(instance) = matched.instance() {
state.serialize_field("instance", instance)?;
}
state.end()
}
}
fn admission_value(matched: &DialectMatch) -> Value {
match matched.admission() {
Admission::Admitted => serde_json::json!("admitted"),
Admission::Unverified { using } => serde_json::json!({
"unverified": {
"using": matched.grammar_id(using)
}
}),
Admission::Residual => serde_json::json!("residual"),
Admission::Refused => serde_json::json!("refused"),
}
}
fn push_dialect_layers_summary(
rows: &mut Vec<(String, SummaryCell)>,
prefix: &str,
layers: Option<&DialectLayers>,
) -> Result<()> {
let Some(layers) = layers else {
rows.push((
format!("{prefix}_dialects"),
serde_json::value::to_raw_value(&())?.into(),
));
return push_dialect_summary(rows, prefix, None);
};
rows.push((
format!("{prefix}_dialect_layers"),
layers.iter().count().to_string().into(),
));
rows.push((
format!("{prefix}_dialects"),
serde_json::value::to_raw_value(&DialectLayersOutput(layers))?.into(),
));
push_dialect_summary(rows, prefix, Some(layers.primary()))
}
fn push_dialect_summary(
rows: &mut Vec<(String, SummaryCell)>,
prefix: &str,
matched: Option<&DialectMatch>,
) -> Result<()> {
let Some(matched) = matched else {
rows.push((format!("{prefix}_dialect"), "null".to_owned().into()));
return Ok(());
};
rows.push((
format!("{prefix}_dialect_format"),
cell(matched.format()).into(),
));
rows.push((
format!("{prefix}_dialect"),
cell(matched.dialect().as_str()).into(),
));
let admission = admission_value(matched);
let admission = admission
.as_str()
.map_or_else(|| admission.to_string(), cell);
rows.push((format!("{prefix}_dialect_admission"), admission.into()));
if let Some(instance) = matched.instance() {
rows.push((format!("{prefix}_dialect_instance"), cell(instance).into()));
}
if !matched.declared().is_empty() {
rows.push((
format!("{prefix}_dialect_declared"),
serde_json::value::to_raw_value(matched.declared())?.into(),
));
}
Ok(())
}
fn count_severity(findings: &[FindingProbe], severities: &[&str]) -> usize {
findings
.iter()
.filter(|finding| {
finding
.severity
.as_deref()
.is_some_and(|severity| severities.contains(&severity))
})
.count()
}
fn coverage(artifact: &Artifact, args: &QueryArgs) -> Result<()> {
note_unvalidated_sidecar_fidelity(artifact);
let decode = match artifact {
Artifact::Report(report) => report.decode_report.as_ref(),
Artifact::Sidecar(sidecar) => sidecar.report.as_ref(),
Artifact::Cadir(_) => bail!(
"a CADIR document has no decode report; coverage is in the report \
written by `dump --report` or in the `.fidelity.json` sidecar"
),
};
let (coverage, note): (&BTreeMap<String, u64>, Option<&str>) = match decode {
Some(decode) if decode.coverage.is_empty() => {
(&decode.coverage, Some("(coverage is empty)"))
}
Some(decode) => (&decode.coverage, None),
None => {
static EMPTY: BTreeMap<String, u64> = BTreeMap::new();
(
&EMPTY,
Some("(no decode report — this command did not decode)"),
)
}
};
if args.json {
print_json("coverage", counts_json(coverage))?;
} else {
println!("measure\tcount");
for (measure, count) in coverage {
println!("{}\t{count}", cell(measure));
}
}
if let Some(note) = note {
eprintln!("{note}");
}
Ok(())
}
fn findings(artifact: &Artifact, args: &QueryArgs) -> Result<()> {
let report = match artifact {
Artifact::Report(report) => report,
Artifact::Cadir(_) => bail!(
"a CADIR document has no findings; run: cadmpeg check FILE -o report.json \
&& cadmpeg query findings report.json"
),
Artifact::Sidecar(_) => bail!(
"a decode sidecar has no check findings; run: cadmpeg check FILE \
-o report.json && cadmpeg query findings report.json \
(a sidecar carries `coverage` and `losses`)"
),
};
let (rows, note): (&[FindingProbe], Option<&str>) = match &report.check_report {
Some(validation) => (&validation.findings, None),
None => (&[], Some("(no check report; this command did not check)")),
};
if args.json {
let payload = rows
.iter()
.map(|finding| {
serde_json::json!({
"check": finding.check,
"severity": finding.severity,
"message": finding.message,
"entity": finding.entity,
})
})
.collect();
print_json("findings", serde_json::Value::Array(payload))?;
} else {
println!("severity\tcheck\tentity\tmessage");
for finding in rows {
println!(
"{}\t{}\t{}\t{}",
opt(finding.severity.as_ref()),
opt(finding.check.as_ref()),
opt(finding.entity.as_ref()),
opt(finding.message.as_ref()),
);
}
}
if let Some(note) = note {
eprintln!("{note}");
}
Ok(())
}
fn losses(artifact: &Artifact, args: &QueryArgs) -> Result<()> {
note_unvalidated_sidecar_fidelity(artifact);
let (rows, export_losses, note): (&[LossProbe], &[LossProbe], Option<&str>) = match artifact {
Artifact::Report(report) => {
let (rows, note): (&[LossProbe], Option<&str>) =
match (&report.check_report, &report.decode_report, &report.summary) {
(Some(validation), _, _) => (&validation.losses, None),
(None, Some(decode), _) => (&decode.losses, None),
(None, None, Some(summary)) => (&summary.losses, None),
(None, None, None) if report.measure.is_some() => (
&[],
Some(
"(this measure report has no decode stage; `query summary` counts \
its unavailable values)",
),
),
(None, None, None) => (
&[],
Some("(this report has no inspect, decode, check, or export stage)"),
),
};
let export_losses = report
.export
.as_ref()
.map_or(&[][..], |export| export.losses.as_slice());
(
rows,
export_losses,
if report.export.is_some() { None } else { note },
)
}
Artifact::Sidecar(sidecar) => match &sidecar.report {
Some(decode) => (&decode.losses, &[], None),
None => (&[], &[], Some("(this sidecar has no decode report)")),
},
Artifact::Cadir(_) => bail!(
"a CADIR document has no loss notes; losses are in the report written \
by `--report` or in the `.fidelity.json` sidecar"
),
};
if args.json {
let payload = rows
.iter()
.chain(export_losses)
.map(|loss| {
serde_json::json!({
"code": loss.code.as_ref().map(LossCodeProbe::display),
"severity": loss.severity,
"message": loss.message,
})
})
.collect();
print_json("losses", serde_json::Value::Array(payload))?;
} else {
println!("severity\tcode\tmessage");
for loss in rows.iter().chain(export_losses) {
println!(
"{}\t{}\t{}",
opt(loss.severity.as_ref()),
loss.code
.as_ref()
.map(|code| cell(&code.display()))
.unwrap_or_default(),
opt(loss.message.as_ref()),
);
}
}
if let Some(note) = note {
eprintln!("{note}");
}
Ok(())
}
fn note_unvalidated_sidecar_fidelity(artifact: &Artifact) {
if matches!(artifact, Artifact::Sidecar(_)) {
eprintln!(
"(sidecar fidelity validation: not run; `cadmpeg query fidelity FILE` validates it)"
);
}
}
fn counts(artifact: &Artifact, args: &QueryArgs) -> Result<()> {
match artifact {
Artifact::Cadir(cadir) => {
let mut rows: Vec<(String, String, u64)> = cadir
.model
.iter()
.map(|(arena, len)| ("model".to_owned(), arena.clone(), len.0))
.collect();
for (namespace, arenas) in &cadir.native {
for (arena, len) in arenas {
rows.push((format!("native.{namespace}"), arena.clone(), len.0));
}
}
if args.json {
let mut map = serde_json::Map::new();
for (namespace, arena, entries) in &rows {
map.insert(format!("{namespace}.{arena}"), serde_json::json!(entries));
}
print_json("counts", serde_json::Value::Object(map))?;
} else {
println!("namespace\tarena\tentries");
for (namespace, arena, entries) in rows {
println!("{}\t{}\t{entries}", cell(&namespace), cell(&arena));
}
}
Ok(())
}
Artifact::Report(report) => {
let (entity_counts, note): (&BTreeMap<String, u64>, Option<&str>) =
match &report.check_report {
Some(validation) => (&validation.entity_counts, None),
None => {
static EMPTY: BTreeMap<String, u64> = BTreeMap::new();
(
&EMPTY,
Some("(no check report; this command did not check)"),
)
}
};
if args.json {
print_json("counts", counts_json(entity_counts))?;
} else {
println!("namespace\tarena\tentries");
for (arena, entries) in entity_counts {
println!("model\t{}\t{entries}", cell(arena));
}
}
if let Some(note) = note {
eprintln!("{note}");
}
Ok(())
}
Artifact::Sidecar(_) => bail!(
"a decode sidecar has no entity counts; use `cadmpeg query coverage` or \
`cadmpeg query losses` on it, or run `cadmpeg check` for entity counts"
),
}
}