#![cfg_attr(
not(test),
deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
)]
use std::collections::HashSet;
use serde_json::{json, Map, Value};
use pmcp_workbook_runtime::{CellEntry, CellMap, CellRole, Dtype, Manifest, Tool};
pub(crate) fn dtype_json_type(dtype: Dtype) -> &'static str {
match dtype {
Dtype::Number => "number",
Dtype::Text => "string",
Dtype::Bool => "boolean",
}
}
fn role_for_seed<'a>(manifest: &'a Manifest, seed_coord: &str) -> Option<&'a CellRole> {
pmcp_workbook_runtime::role_for_cell(manifest, seed_coord)
}
fn output_column_schema(unit: Option<&str>, role: Option<&CellRole>) -> Value {
let dtype = role.map_or(Dtype::Number, |r| r.dtype);
let mut value_prop = Map::new();
value_prop.insert("type".to_string(), json!(dtype_json_type(dtype)));
if let Some(u) = unit {
value_prop.insert("unit".to_string(), json!(u));
}
let mut props = Map::new();
props.insert("value".to_string(), Value::Object(value_prop));
props.insert("unit".to_string(), json!({ "type": ["string", "null"] }));
let mut col = Map::new();
col.insert("type".to_string(), json!("object"));
col.insert("additionalProperties".to_string(), json!(false));
col.insert("properties".to_string(), Value::Object(props));
col.insert("required".to_string(), json!(["value"]));
if let Some(meaning) = role.and_then(|r| r.meaning.as_deref()) {
col.insert("description".to_string(), json!(meaning));
}
Value::Object(col)
}
#[must_use]
pub fn output_schema_for_manifest(manifest: &Manifest, cell_map: &CellMap) -> Value {
let all_outputs: Vec<CellEntry> = cell_map
.tools
.iter()
.flat_map(|t| t.outputs.iter().cloned())
.collect();
let output_props = output_props_for_entries(manifest, &all_outputs);
let mut success = Map::new();
success.insert(
"outputs".to_string(),
json!({
"type": "object",
"additionalProperties": false,
"properties": Value::Object(output_props),
}),
);
success.insert(
"accepted_overrides".to_string(),
json!({ "type": "array", "items": { "type": "string" } }),
);
result_envelope_schema(success)
}
fn output_props_for_entries(manifest: &Manifest, outputs: &[CellEntry]) -> Map<String, Value> {
let mut output_props = Map::new();
for entry in outputs {
let role = role_for_seed(manifest, &entry.seed_coord);
output_props.insert(
entry.json_key.clone(),
output_column_schema(entry.unit.as_deref(), role),
);
}
output_props
}
#[must_use]
pub fn output_schema_for_tool(manifest: &Manifest, tool: &Tool) -> Value {
let output_props = output_props_for_entries(manifest, &tool.outputs);
let mut success = Map::new();
success.insert(
"outputs".to_string(),
json!({
"type": "object",
"additionalProperties": false,
"properties": Value::Object(output_props),
}),
);
success.insert(
"accepted_overrides".to_string(),
json!({ "type": "array", "items": { "type": "string" } }),
);
result_envelope_schema(success)
}
#[must_use]
pub fn result_envelope_schema(success_props: Map<String, Value>) -> Value {
let mut props = success_props;
props.insert("isError".to_string(), json!({ "type": "boolean" }));
props.insert("code".to_string(), json!({ "type": "string" }));
props.insert("reason".to_string(), json!({ "type": "string" }));
props.insert("field".to_string(), json!({ "type": "string" }));
props.insert(
"allowed".to_string(),
json!({ "type": "array", "items": {} }),
);
props.insert("range".to_string(), json!({ "type": "array" }));
props.insert(
"required".to_string(),
json!({ "type": "array", "items": { "type": "string" } }),
);
props.insert("provenance".to_string(), provenance_schema());
json!({
"type": "object",
"additionalProperties": true,
"properties": Value::Object(props),
"required": ["provenance"],
})
}
#[must_use]
pub fn explain_output_schema() -> Value {
let mut success = Map::new();
success.insert(
"steps".to_string(),
json!({
"type": "array",
"description": "Ordered business-language derivation steps.",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"step": { "type": "string" },
"cell": { "type": "string" },
},
},
}),
);
success.insert(
"annotations".to_string(),
json!({
"type": "object",
"description": "Manifest-declared annotations (keyed by AnnotationDecl name).",
"additionalProperties": { "type": "object" },
}),
);
result_envelope_schema(success)
}
#[must_use]
pub fn get_manifest_output_schema() -> Value {
let mut success = Map::new();
success.insert("bundle_id".to_string(), json!({ "type": "string" }));
success.insert("version".to_string(), json!({ "type": "string" }));
success.insert("combined_hash".to_string(), json!({ "type": "string" }));
for field in ["inputs", "outputs", "governed_data", "changelog"] {
success.insert(
field.to_string(),
json!({ "type": "array", "items": { "type": "object" } }),
);
}
result_envelope_schema(success)
}
#[must_use]
pub fn diff_version_output_schema() -> Value {
let mut success = Map::new();
success.insert("from_version".to_string(), json!({ "type": "string" }));
success.insert("to_version".to_string(), json!({ "type": "string" }));
success.insert(
"deltas".to_string(),
json!({
"type": "array",
"description": "Per-output change records (region, change class, old/new \
meaning+unit+provenance, drift/redefinition severity).",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"region": { "type": "string" },
"change_class": { "type": "string" },
"severity": { "type": "string" },
},
},
}),
);
success.insert(
"summary".to_string(),
json!({ "type": "string", "description": "Human-readable transition summary." }),
);
result_envelope_schema(success)
}
#[must_use]
pub fn render_workbook_output_schema() -> Value {
let mut success = Map::new();
success.insert(
"resource_uri".to_string(),
json!({
"type": "string",
"description": "A provenance-bound workbook:// resource URI. Read it via \
resources/read to obtain the base64-encoded .xlsx, which is \
regenerated statelessly from the URI on each read. The URI \
encodes the inputs — treat it as sensitive.",
}),
);
success.insert(
"mime_type".to_string(),
json!({
"type": "string",
"description": "The MIME type of the resource the URI resolves to (the OOXML \
spreadsheet type).",
}),
);
result_envelope_schema(success)
}
#[must_use]
pub fn provenance_schema() -> Value {
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"bundle_id": { "type": "string" },
"version": { "type": "string" },
"combined_hash": { "type": "string" },
},
"required": ["bundle_id", "version", "combined_hash"],
})
}
#[must_use]
pub fn input_schema_for_manifest(manifest: &Manifest, cell_map: &CellMap) -> Value {
let mut input_props = Map::new();
for entry in &cell_map.inputs {
input_props.insert(
entry.json_key.clone(),
input_prop_for_entry(manifest, entry),
);
}
assemble_input_schema(manifest, input_props)
}
#[must_use]
pub fn input_schema_for_tool(manifest: &Manifest, cell_map: &CellMap, tool: &Tool) -> Value {
let reached: HashSet<&str> = tool.input_keys.iter().map(String::as_str).collect();
let project_all = tool.input_keys.is_empty();
let mut input_props = Map::new();
for entry in &cell_map.inputs {
if project_all || reached.contains(entry.json_key.as_str()) {
input_props.insert(
entry.json_key.clone(),
input_prop_for_entry(manifest, entry),
);
}
}
assemble_input_schema(manifest, input_props)
}
fn input_prop_for_entry(manifest: &Manifest, entry: &CellEntry) -> Value {
let role = role_for_seed(manifest, &entry.seed_coord);
let dtype = role.map_or(Dtype::Number, |r| r.dtype);
let mut prop = Map::new();
prop.insert("type".to_string(), json!(dtype_json_type(dtype)));
if let Some(unit) = entry.unit.as_deref() {
prop.insert("unit".to_string(), json!(unit));
}
if let Some(meaning) = role.and_then(|r| r.meaning.as_deref()) {
prop.insert("description".to_string(), json!(meaning));
}
if let Some(allowed) = role.and_then(|r| r.allowed_values.as_ref()) {
prop.insert("enum".to_string(), json!(allowed));
}
Value::Object(prop)
}
fn assemble_input_schema(manifest: &Manifest, input_props: Map<String, Value>) -> Value {
let mut override_props = Map::new();
for key in crate::workbook::input::variable_tier_keys(manifest) {
override_props.insert(
key,
json!({ "type": ["number", "string", "boolean", "null"] }),
);
}
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"inputs": {
"type": "object",
"additionalProperties": false,
"properties": Value::Object(input_props),
},
"overrides": {
"type": "object",
"additionalProperties": { "type": ["number", "string", "boolean", "null"] },
"properties": Value::Object(override_props),
"description": "Variable-tier parameter overrides, keyed by parameter \
name or cell key. Strict (BA-governed) constants are rejected.",
},
},
})
}
#[must_use]
pub fn empty_input_schema() -> Value {
json!({ "type": "object", "additionalProperties": false })
}
#[cfg(test)]
mod tests {
use super::*;
use pmcp_workbook_runtime::CellValue;
use pmcp_workbook_runtime::{CellEntry, CellMap, InputTier, Role, Tool};
fn input_role(
cell: &str,
dtype: Dtype,
meaning: &str,
allowed: Option<Vec<String>>,
) -> CellRole {
CellRole {
cell: cell.to_string(),
role: Role::Input,
name: None,
unit: Some("USD".to_string()),
meaning: Some(meaning.to_string()),
dtype,
colour_evidence: None,
source: "test".to_string(),
notes: None,
tier: Some(InputTier::Variable {
default: CellValue::Number(0.0),
}),
allowed_values: allowed,
}
}
fn output_role(cell: &str, meaning: &str) -> CellRole {
CellRole {
cell: cell.to_string(),
role: Role::Output,
name: None,
unit: Some("USD".to_string()),
meaning: Some(meaning.to_string()),
dtype: Dtype::Number,
colour_evidence: None,
source: "test".to_string(),
notes: None,
tier: None,
allowed_values: None,
}
}
fn manifest_with(cells: Vec<CellRole>) -> Manifest {
Manifest {
schema_version: 1,
workflow: "tax-calc".to_string(),
workbook_hash: None,
ratified: true,
ratified_by: None,
ratified_at: None,
cells,
loop_block: None,
governed_data: vec![],
changelog: vec![],
capability_calls: vec![],
annotations: vec![],
}
}
fn three_input_manifest_and_map() -> (Manifest, CellMap) {
let manifest = manifest_with(vec![
input_role("1_Inputs!B2", Dtype::Number, "Gross income", None),
input_role(
"1_Inputs!B3",
Dtype::Text,
"Filing status",
Some(vec!["single".to_string(), "married_joint".to_string()]),
),
input_role("1_Inputs!B4", Dtype::Number, "Deductions", None),
output_role("3_Outputs!B2", "Taxable income"),
output_role("3_Outputs!B3", "Tax owed"),
]);
let cell_map = CellMap {
inputs: vec![
CellEntry {
json_key: "gross_income".to_string(),
seed_coord: "1_Inputs!B2".to_string(),
unit: Some("USD".to_string()),
},
CellEntry {
json_key: "filing_status".to_string(),
seed_coord: "1_Inputs!B3".to_string(),
unit: None,
},
CellEntry {
json_key: "deductions".to_string(),
seed_coord: "1_Inputs!B4".to_string(),
unit: Some("USD".to_string()),
},
],
tools: vec![Tool {
name: "calculate".to_string(),
description: None,
input_keys: Vec::new(),
outputs: vec![
CellEntry {
json_key: "taxable_income".to_string(),
seed_coord: "3_Outputs!B2".to_string(),
unit: Some("USD".to_string()),
},
CellEntry {
json_key: "tax_owed".to_string(),
seed_coord: "3_Outputs!B3".to_string(),
unit: Some("USD".to_string()),
},
],
oracle: std::collections::BTreeMap::new(),
}],
};
(manifest, cell_map)
}
#[test]
fn input_schema_is_strict_and_projects_all_inputs() {
let (m, cm) = three_input_manifest_and_map();
let schema = input_schema_for_manifest(&m, &cm);
assert_eq!(schema["additionalProperties"], false);
let props = &schema["properties"]["inputs"]["properties"];
assert_eq!(props["gross_income"]["type"], json!("number"));
assert_eq!(props["gross_income"]["unit"], json!("USD"));
assert_eq!(props["gross_income"]["description"], json!("Gross income"));
assert_eq!(props["filing_status"]["type"], json!("string"));
assert_eq!(props["deductions"]["type"], json!("number"));
assert_eq!(
schema["properties"]["inputs"]["additionalProperties"],
false
);
}
#[test]
fn input_schema_emits_enum_for_allowed_values_and_keeps_it_optional() {
let (m, cm) = three_input_manifest_and_map();
let schema = input_schema_for_manifest(&m, &cm);
let props = &schema["properties"]["inputs"]["properties"];
assert_eq!(
props["filing_status"]["enum"],
json!(["single", "married_joint"]),
"allowed_values surfaces as a JSON-Schema enum (verbatim order)"
);
assert!(props["gross_income"].get("enum").is_none());
assert!(schema["properties"]["inputs"].get("required").is_none());
}
#[test]
fn output_schema_is_non_empty_and_carries_every_named_output() {
let (m, cm) = three_input_manifest_and_map();
let schema = output_schema_for_manifest(&m, &cm);
let outputs = &schema["properties"]["outputs"]["properties"];
assert!(outputs["taxable_income"].is_object());
assert!(outputs["tax_owed"].is_object());
assert_eq!(
outputs["taxable_income"]["properties"]["value"]["unit"],
"USD"
);
assert_eq!(
outputs["taxable_income"]["properties"]["value"]["type"],
"number"
);
let headline_key = ["supply", "_", "total"].concat();
assert!(
schema["properties"].get(&headline_key).is_none(),
"no privileged headline field at the root (S-1)"
);
assert!(schema["properties"]["provenance"].is_object());
assert!(
!outputs
.as_object()
.expect("outputs is an object")
.is_empty(),
"outputSchema must enumerate at least one output"
);
}
#[test]
fn result_envelope_accepts_both_success_and_iserror_shapes() {
let (m, cm) = three_input_manifest_and_map();
let schema = output_schema_for_manifest(&m, &cm);
assert_eq!(schema["properties"]["isError"]["type"], "boolean");
assert_eq!(schema["properties"]["code"]["type"], "string");
assert_eq!(schema["properties"]["reason"]["type"], "string");
assert_eq!(schema["additionalProperties"], true);
assert_eq!(schema["required"], json!(["provenance"]));
}
#[test]
fn overrides_advertise_variable_tier_keys() {
let (m, cm) = three_input_manifest_and_map();
let schema = input_schema_for_manifest(&m, &cm);
let override_props = &schema["properties"]["overrides"]["properties"];
let props = override_props
.as_object()
.expect("overrides.properties is an object");
for key in ["1_Inputs!B2", "1_Inputs!B3", "1_Inputs!B4"] {
assert!(
props.contains_key(key),
"overrides advertises the variable-tier key `{key}` (got {props:?})"
);
assert_eq!(
override_props[key]["type"],
json!(["number", "string", "boolean", "null"]),
"each advertised override carries the permissive value-type union"
);
}
assert!(
!props.contains_key("3_Outputs!B2") && !props.contains_key("taxable_income"),
"a computed output is never an advertised override key"
);
}
#[test]
fn overrides_advertise_named_param_keys() {
let mut named = input_role("1_Inputs!B2", Dtype::Number, "Gross income", None);
named.name = Some("in_gross_income".to_string());
let m = manifest_with(vec![named, output_role("3_Outputs!B2", "Taxable income")]);
let cm = CellMap {
inputs: vec![CellEntry {
json_key: "gross_income".to_string(),
seed_coord: "1_Inputs!B2".to_string(),
unit: Some("USD".to_string()),
}],
tools: vec![Tool {
name: "calculate".to_string(),
description: None,
input_keys: Vec::new(),
outputs: vec![CellEntry {
json_key: "taxable_income".to_string(),
seed_coord: "3_Outputs!B2".to_string(),
unit: Some("USD".to_string()),
}],
oracle: std::collections::BTreeMap::new(),
}],
};
let schema = input_schema_for_manifest(&m, &cm);
let override_props = &schema["properties"]["overrides"]["properties"];
assert!(
override_props["in_gross_income"].is_object(),
"the named variable-tier param is advertised under its name"
);
}
#[test]
fn overrides_keep_open_additional_properties_for_discoverability_only() {
let (m, cm) = three_input_manifest_and_map();
let schema = input_schema_for_manifest(&m, &cm);
let overrides = &schema["properties"]["overrides"];
assert_eq!(
overrides["additionalProperties"],
json!({ "type": ["number", "string", "boolean", "null"] }),
"the open value-typed additionalProperties map is preserved"
);
assert!(
overrides["description"].as_str().is_some(),
"the prose override description is retained"
);
}
#[test]
fn provenance_schema_uses_combined_hash_never_workbook_hash() {
let schema = provenance_schema();
let props = &schema["properties"];
assert!(props["combined_hash"].is_object());
assert!(
props.get("workbook_hash").is_none(),
"the provenance schema must never carry workbook_hash (Codex HIGH #3)"
);
assert_eq!(
schema["required"],
json!(["bundle_id", "version", "combined_hash"])
);
}
#[test]
fn empty_input_keys_projects_full_pool() {
let (m, cm) = three_input_manifest_and_map();
let tool = &cm.tools[0];
assert!(
tool.input_keys.is_empty(),
"the fixture tool has no derived keys"
);
let schema = input_schema_for_tool(&m, &cm, tool);
let props = schema["properties"]["inputs"]["properties"]
.as_object()
.expect("inputs.properties object");
assert!(
!props.is_empty(),
"an empty-input_keys tool advertises the full pool, not an empty schema"
);
for key in ["gross_income", "filing_status", "deductions"] {
assert!(
props.contains_key(key),
"the full shared-input pool is advertised: missing {key}"
);
}
assert_eq!(
schema["properties"]["inputs"]["additionalProperties"],
json!(false),
"the strict per-tool envelope is preserved"
);
}
#[test]
fn populated_input_keys_projects_only_reached() {
let (m, mut cm) = three_input_manifest_and_map();
cm.tools[0].input_keys = vec!["gross_income".to_string()];
let schema = input_schema_for_tool(&m, &cm, &cm.tools[0]);
let props = schema["properties"]["inputs"]["properties"]
.as_object()
.expect("inputs.properties object");
assert_eq!(props.len(), 1, "only the reached key is projected");
assert!(props.contains_key("gross_income"));
assert!(!props.contains_key("deductions"));
}
}