///|
/// Versioned data interchange. Units and provenance are caller-managed;
/// this schema deliberately serializes measured coordinates and values only.
pub fn Spectrum::to_json(self : Spectrum) -> Json {
Json::object({
"schema": Json::number(1.0),
"axis": @json.to_json(self.axis),
"values": @json.to_json(self.values),
})
}
///|
fn require_keys(
value : Json,
keys : Array[String],
) -> Map[String, Json] raise SpectralError {
let fields = match value {
Json::Object(fields) => fields
_ => raise Invalid("JSON object required")
}
if fields.length() != keys.length() {
raise Invalid("JSON fields differ from schema")
}
for key in keys {
if !fields.contains(key) {
raise Invalid("missing JSON field: " + key)
}
}
match fields.get("schema") {
Some(schema) =>
if !(schema is Json::Number(1.0, ..)) {
raise Invalid("unsupported JSON schema version")
}
None => raise Invalid("missing JSON schema")
}
fields
}
///|
fn required_field(
fields : Map[String, Json],
key : String,
) -> Json raise SpectralError {
match fields.get(key) {
Some(value) => value
None => raise Invalid("missing JSON field: " + key)
}
}
///|
/// Validate lengths before allocating a second numeric copy; then use the
/// same spectrum constructor invariants as direct API input.
pub fn spectrum_from_json(value : Json) -> Spectrum raise SpectralError {
let fields = require_keys(value, ["schema", "axis", "values"])
let x = match required_field(fields, "axis") {
Json::Array(items) => items
_ => raise Invalid("axis must be a JSON array")
}
let y = match required_field(fields, "values") {
Json::Array(items) => items
_ => raise Invalid("values must be a JSON array")
}
if x.length() != y.length() {
raise Invalid("JSON array lengths differ")
}
if x.length() < 3 || x.length() > 65536 {
raise Budget("JSON spectrum needs 3..65536 samples")
}
let axis = Array::make(x.length(), 0.0)
let values = Array::make(y.length(), 0.0)
for i in 0.. v
_ => raise Invalid("axis item must be numeric")
}
values[i] = match y[i] {
Json::Number(v, ..) => v
_ => raise Invalid("value item must be numeric")
}
}
spectrum(axis, values)
}
///|
/// Bounded parser for untrusted spectrum text; no I/O or filesystem access.
pub fn parse_spectrum(text : String) -> Spectrum raise SpectralError {
if text.length() > 8388608 {
raise Budget("JSON input exceeds 8 Mi UTF-16 code units")
}
let mut depth = 0
let mut in_string = false
let mut escaped = false
for ch in text.iter() {
if in_string {
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == '"' {
in_string = false
}
} else if ch == '"' {
in_string = true
} else if ch == '[' || ch == '{' {
depth = depth + 1
if depth > 8 {
raise Budget("JSON nesting exceeds eight levels")
}
} else if ch == ']' || ch == '}' {
depth = depth - 1
}
}
let parsed = @json.parse(text) catch {
_ => raise Invalid("malformed spectrum JSON")
}
spectrum_from_json(parsed)
}
///|
/// Persist only the frozen reference; derived regression statistics are
/// recomputed and validated on load rather than trusting serialized numbers.
pub fn ScatterModel::to_json(self : ScatterModel) -> Json {
Json::object({
"schema": Json::number(1.0),
"reference": self.reference.to_json(),
})
}
///|
pub fn scatter_model_from_json(
value : Json,
) -> ScatterModel raise SpectralError {
let fields = require_keys(value, ["schema", "reference"])
scatter_reference(spectrum_from_json(required_field(fields, "reference")))
}
///|
fn optional_number(value : Double?) -> Json {
match value {
Some(v) => Json::number(v)
None => Json::null()
}
}
///|
fn optional_bool(value : Bool?) -> Json {
match value {
Some(v) => Json::boolean(v)
None => Json::null()
}
}
///|
/// Compact deterministic report; no intermediate spectra, environment paths,
/// account credentials, timestamps or private metadata are collected.
pub fn PipelineResult::to_json(self : PipelineResult) -> Json {
let trace = self.trace.map(fn(item) {
Json::object({
"name": Json::string(item.name),
"input_samples": Json::number(item.input_samples.to_double()),
"output_samples": Json::number(item.output_samples.to_double()),
"rms_before": Json::number(item.rms_before),
"rms_after": Json::number(item.rms_after),
"iterations": match item.iterations {
Some(n) => Json::number(n.to_double())
None => Json::null()
},
"converged": optional_bool(item.converged),
"relative_change": optional_number(item.relative_change),
})
})
Json::object({
"schema": Json::number(1.0),
"spectrum": self.spectrum.to_json(),
"trace": Json::array(trace),
"estimated_work": Json::number(self.estimated_work),
})
}