///|
/// Read one JSONL line from an engine's stdout stream back into an `Event`.
///
/// The inverse of `Event::to_json`, pinned per variant by `parse_test`.
///
/// `None` means "not an event this version reads" — an unknown `event` name, a
/// missing `event` key, or a payload whose required fields are absent or
/// mistyped. A client stays tolerant of a newer engine by skipping such a line
/// rather than failing on it, which matters because the TUI and the desktop
/// host both launch whichever `loci` is on PATH.
///
/// Only fields that a renderer treats as genuinely optional are defaulted:
/// `picked_up_from` and `recommendation` (a cold entry and a session with no
/// advice are ordinary), and the two string lists (empty and absent mean the
/// same to every renderer). Everything else is required, because its absence
/// means the line is not what it claims to be.
pub fn parse_event(line : Json) -> Event? {
guard line is Object(fields) else { return None }
guard text(fields, "event") is Some(name) else { return None }
match name {
"ready" =>
match (int(fields, "version"), text(fields, "engine")) {
(Some(version), Some(engine)) => Some(Ready(version~, engine~))
_ => None
}
"entered" =>
match
(
object(fields, "locus").bind(parse_locus_fields),
object(fields, "trail").bind(parse_trail_fields),
) {
(Some(locus), Some(trail)) => Some(Entered(locus~, trail~))
_ => None
}
"trail_listed" =>
object(fields, "trail")
.bind(parse_trail_fields)
.map(trail => TrailListed(trail~))
"located" =>
match fields.get("loci") {
Some(Array(items)) => {
let loci : Array[Locus] = []
for item in items {
guard item is Object(nested) else { return None }
guard parse_locus_fields(nested) is Some(locus) else { return None }
loci.push(locus)
}
Some(Located(loci~))
}
_ => None
}
"residue_filed" =>
object(fields, "residue")
.bind(parse_residue_fields)
.map(residue => ResidueFiled(residue~))
"sealed" =>
match (int(fields, "epoch"), text(fields, "root")) {
(Some(epoch), Some(root)) => Some(Sealed(epoch~, root~))
_ => None
}
"stored" =>
match (text(fields, "digest"), text(fields, "disposition")) {
(Some(digest), Some(disposition)) => Some(Stored(digest~, disposition~))
_ => None
}
"diffed" =>
match
(
int(fields, "added"),
int(fields, "removed"),
int(fields, "changed"),
int(fields, "unchanged"),
) {
(Some(added), Some(removed), Some(changed), Some(unchanged)) =>
Some(Diffed(added~, removed~, changed~, unchanged~))
_ => None
}
"diagnostic" =>
match
(
text(fields, "severity").bind(Severity::parse),
text(fields, "message"),
) {
(Some(severity), Some(message)) => Some(Diagnostic(severity~, message~))
_ => None
}
"finished" => int(fields, "code").map(code => Finished(code~))
_ => None
}
}
///|
/// Read one JSONL line from a surface's stdin stream back into a `Command`.
///
/// `None` carries the same meaning as in `parse_event`: a line this version
/// does not read, to be skipped rather than treated as a failure.
pub fn parse_command(line : Json) -> Command? {
guard line is Object(fields) else { return None }
guard text(fields, "command") is Some(name) else { return None }
match name {
"enter" => text(fields, "locus").map(locus => Enter(locus~))
"trail" => text(fields, "locus").map(locus => Trail(locus~))
"locate" => Some(Locate)
"sign" =>
object(fields, "residue")
.bind(parse_residue_fields)
.map(residue => Sign(residue~))
"interrupt" => Some(Interrupt)
"shutdown" => Some(Shutdown)
_ => None
}
}
///|
/// Read a residue payload from its already-destructured object fields.
pub fn parse_residue(value : Json) -> Residue? {
guard value is Object(fields) else { return None }
parse_residue_fields(fields)
}
///|
/// Read a locus payload from its already-destructured object fields.
pub fn parse_locus(value : Json) -> Locus? {
guard value is Object(fields) else { return None }
parse_locus_fields(fields)
}
///|
/// Read a trail payload from its already-destructured object fields.
pub fn parse_trail(value : Json) -> Trail? {
guard value is Object(fields) else { return None }
parse_trail_fields(fields)
}
///|
fn parse_residue_fields(fields : Map[String, Json]) -> Residue? {
guard text(fields, "locus") is Some(locus) else { return None }
guard text(fields, "tier") is Some(tier) else { return None }
guard text(fields, "session_short_id") is Some(session_short_id) else {
return None
}
guard text(fields, "timestamp") is Some(timestamp) else { return None }
Some({
locus,
tier,
session_short_id,
timestamp,
picked_up_from: opt_text(fields, "picked_up_from"),
what_i_did: strings(fields, "what_i_did"),
what_i_left_open: strings(fields, "what_i_left_open"),
recommendation: opt_text(fields, "recommendation"),
})
}
///|
fn parse_locus_fields(fields : Map[String, Json]) -> Locus? {
guard text(fields, "name") is Some(name) else { return None }
guard text(fields, "spirit") is Some(spirit) else { return None }
guard int(fields, "trail_depth") is Some(trail_depth) else { return None }
let last_visit = match fields.get("last_visit") {
Some(Object(nested)) =>
match parse_residue_fields(nested) {
Some(residue) => Some(residue)
// A present-but-malformed residue is a broken line, not an unvisited
// locus; reporting it as "never entered" would hide the break.
None => return None
}
_ => None
}
Some({ name, spirit, tags: strings(fields, "tags"), trail_depth, last_visit, })
}
///|
fn parse_trail_fields(fields : Map[String, Json]) -> Trail? {
guard text(fields, "locus") is Some(locus) else { return None }
let sessions : Array[Residue] = []
match fields.get("sessions") {
Some(Array(items)) =>
for item in items {
guard item is Object(nested) else { return None }
guard parse_residue_fields(nested) is Some(residue) else { return None }
sessions.push(residue)
}
_ => return None
}
Some({ locus, sessions, })
}