Sign in

    loci_protocol

    Typed Loci surface contract: the values every Loci surface renders and the events and commands they exchange.

    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    14 hours ago
    Downloads
    1

    #zploc/loci_protocol

    The values every Loci surface renders, and the events and commands they exchange with a Loci engine.

    #Why this is its own module

    protocol is a leaf: it imports moonbitlang/core/json and nothing else, least of all anything from zploc/loci. That is what lets the desktop frontend — which compiles to JS and cannot link the native substrate — decode the same stream the TUI reads and the CLI writes.

    It is a workspace member in the root moon.work, so every surface builds against the working-tree copy rather than a published snapshot. A change to the wire shape is a compile error in all of them at once, which is the only reason the three stay in agreement.

    #The shape

    Two directions, one line of JSON each:

    • Event — engine to surface, on stdout.
    • Command — surface to engine, on stdin.

    Both are flat objects tagged by a "event" or "command" key.

    {"event":"entered","locus":{…},"trail":{…}} {"command":"sign","residue":{…}}

    Event::to_json is the only writer and parse_event its inverse, pinned per variant by parse_test.mbt. A variant added without a decoder arm fails there rather than in a surface.

    #Values, not text

    The events carry Locus, Trail and Residue — the projections of their @model counterparts with the markdown codecs and content hashes dropped.

    They deliberately do not carry rendered text. TrailListed hands over the residue chain, never a box-drawn panel. That is the whole point: the CLI prints a box, the TUI lays out a pane, and the desktop emits DOM, all from one value. Before this module the box-drawing lived in locus/, so the terminal was the only surface that could exist.

    #Reading a stream you do not fully understand

    parse_event returns None for a line this version does not read: an unknown event name, a missing tag, or a payload whose required fields are absent or mistyped. Clients skip those lines rather than failing, because the TUI and the desktop host both launch whichever loci is on PATH and may be newer or older than it.

    Only fields a renderer treats as genuinely optional are defaulted — a residue's picked_up_from and recommendation, and its two string lists. Everything else is required: its absence means the line is not what it claims to be, and guessing there would hide a real wire break behind a fabricated value.

    Command

    pub(all) enum Command {
    Enter(locus~ : String)
    Trail(locus~ : String)
    Locate
    Sign(residue~ : Residue)
    Interrupt
    Shutdown
    } derive(Eq,
    Debug
    )

    One line on a Loci engine's stdin JSONL stream: what a surface asks the engine to do.

    The reverse direction of Event, and deliberately much smaller. A surface drives inhabitation and asks for reads; everything else it learns by watching the event stream.

    Command::to_json

    fn Command::to_json(self : Command) -> Json

    The command as one line's worth of JSON.

    Event

    pub(all) enum Event {
    Ready(version~ : Int, engine~ : String)
    Entered(locus~ : Locus, trail~ : Trail)
    TrailListed(trail~ : Trail)
    Located(loci~ : Array[Locus])
    ResidueFiled(residue~ : Residue)
    Sealed(epoch~ : Int, root~ : String)
    Stored(digest~ : String, disposition~ : String)
    Diffed(added~ : Int, removed~ : Int, changed~ : Int, unchanged~ : Int)
    Diagnostic(severity~ : Severity, message~ : String)
    Finished(code~ : Int)
    } derive(Eq,
    Debug
    )

    One line on a Loci engine's stdout JSONL stream: the contract between loci serve and every client that reads it — the TUI, the desktop host, and any script piping the stream.

    Events carry rendered values, never rendered text. Trail hands over the residue chain, not a box-drawn panel, which is what lets three surfaces present the same run without any of them reimplementing the walk.

    to_json is the only writer and parse its inverse; parse_test pins the round trip per variant.

    Event::to_json

    fn Event::to_json(self : Event) -> Json

    The event as one line's worth of JSON: its "event" name plus its payload, flat.

    This is the wire shape and the only place it is written. Two writers of one event must not spell it two ways.

    Locus

    pub(all) struct Locus {
    name : String
    spirit : String
    tags : Array[String]
    trail_depth : Int
    last_visit : Residue?
    } derive(Eq,
    Debug
    )

    A locus and, when it has been visited, its most recent residue.

    Locus::to_json

    fn Locus::to_json(self : Locus) -> Json

    The locus as the wire carries it.

    Residue

    pub(all) struct Residue {
    locus : String
    tier : String
    session_short_id : String
    timestamp : String
    picked_up_from : String?
    what_i_did : Array[String]
    what_i_left_open : Array[String]
    recommendation : String?
    } derive(Eq,
    Debug
    )

    One recorded visit to a locus, flattened for display.

    The projection of @model.Residue that crosses a surface boundary: the domain record additionally carries markdown encode/decode and content hashes, none of which a renderer needs. Keeping the rendered shape separate is what lets the CLI print a box, the TUI lay out a pane, and the desktop emit DOM from one value instead of three.

    Residue::filename

    fn Residue::filename(self : Residue) -> String

    Canonical residue filename, matching @model.Residue::filename.

    Residue::to_json

    fn Residue::to_json(self : Residue) -> Json

    The residue as the wire carries it.

    Severity

    pub(all) enum Severity {
    Debug
    Info
    Warn
    Error
    } derive(Eq,
    Debug
    )

    How loudly a surface should present a diagnostic.

    Owned by the event that carries it rather than chosen at the call site, so one condition cannot be reported at two severities by two emitters.

    Severity::parse

    fn Severity::parse(token : String) -> Severity?

    Inverse of token; None for a token this version does not know.

    Severity::token

    fn Severity::token(self : Severity) -> String

    Stable lowercase wire token.

    Trail

    pub(all) struct Trail {
    locus : String
    sessions : Array[Residue]
    } derive(Eq,
    Debug
    )

    Every residue filed against one locus, newest first.

    Trail::open_threads

    fn Trail::open_threads(self : Trail) -> Array[String]

    Open threads carried by the newest residue, or an empty array when the locus is unvisited or the last session closed everything.

    The question every surface asks on entry, so it is answered once here rather than re-derived by each of them.

    Trail::to_json

    fn Trail::to_json(self : Trail) -> Json

    The trail as the wire carries it.

    parse_command

    fn parse_command(line : Json) -> Command?

    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.

    parse_event

    fn parse_event(line : Json) -> Event?

    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.

    parse_locus

    fn parse_locus(value : Json) -> Locus?

    Read a locus payload from its already-destructured object fields.

    parse_residue

    fn parse_residue(value : Json) -> Residue?

    Read a residue payload from its already-destructured object fields.

    parse_trail

    fn parse_trail(value : Json) -> Trail?

    Read a trail payload from its already-destructured object fields.