Documentation
¶
Overview ¶
Package goaxi is a hardening and guard layer over the official TOON codec, github.com/toon-format/toon-go.
It is deliberately NOT a codec. toon-go encodes and decodes TOON; this package adds the four things toon-go does not cover, which every agent-facing CLI in this ecosystem needs and currently either duplicates or lacks entirely:
Sanitize: hostile input handling. toon-go rejects control bytes below 0x20 with an error but passes U+2028/U+2029, invalid UTF-8, and lone C1 bytes straight through. A payload carrying a raw ANSI escape reaches the terminal that renders it.
Check: toon-go supports neither defined string types nor encoding.TextMarshaler, and a violating type does not error — it emits EMPTY output. A command silently prints nothing. Check turns that into a detectable condition, and additionally reports when TOON is larger than JSON for a given shape.
Exit codes: one canonical set, as exported constants rather than a rule written in a style guide.
Help blocks: one canonical writer for AXI contextual-disclosure lines.
The constants-over-prose choice is deliberate. A documented convention drifts; a compiled constant cannot.
Index ¶
- func CanEncode(v any) bool
- func Encode(w io.Writer, v any) error
- func EncodeOrJSON(w io.Writer, v any) error
- func MustSanitize(v any) any
- func Sanitize(v any) (any, error)
- func SanitizeString(s string) string
- func WriteHelp(w io.Writer, lines []string) error
- type ExitCode
- type KeyCollisionError
- type Tier
- type Verdict
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CanEncode ¶
CanEncode reports whether v survives TOON encoding without losing data. It is the boolean form of Check for callers that do not need the detail.
func Encode ¶
Encode sanitizes v and writes it as TOON, terminated by exactly one newline.
Sanitizing here rather than leaving it to the caller is deliberate. The one time a caller forgets is the time a control byte reaches a terminal, and a control byte is exactly what arrives when a tool echoes back text it did not author.
Nothing is written when encoding fails. A partial payload is worse than no payload, because it parses.
func EncodeOrJSON ¶
EncodeOrJSON writes v as TOON when that is lossless, and otherwise falls back to a self-describing JSON envelope naming the reason.
The fallback is triggered only by data loss, never by size. Routing on cost would make a command's output format vary with payload length, which no consumer could predict and every consumer would have to handle both ways.
Note for cadence: its STYLE.md prohibits runtime format detection, fixing the format per command at design time. This function is that prohibited thing, so cadence should keep its build-time guards and simply not call it. It exists for callers that have no such rule.
func MustSanitize ¶
MustSanitize is Sanitize for callers whose map keys are fixed identifiers, where a cleaning-induced collision is impossible by construction. It panics if one occurs, because continuing would mean emitting a payload with a field missing.
func Sanitize ¶
Sanitize returns a copy of v with every string cleaned of characters that would either break the TOON encoder or reach stdout as a raw control byte.
It exists because toon-go mishandles hostile input in two opposite directions, both confirmed against the pinned version rather than assumed:
- A raw ANSI escape (\x1b) makes toon.Marshal return an error, so a command handed reviewer text containing colour codes emits nothing at all.
- U+2028, U+2029, lone C1 bytes (0x9b, 0x9d) and invalid UTF-8 pass through untouched, reaching whatever terminal renders the output.
Sanitizing before encoding fixes both: the encoder never sees a byte it would reject, and never sees one it would forward.
Characters are STRIPPED rather than replaced, so text on either side of a removed byte joins contiguously. Substituting a space would silently alter content; a reader cannot tell an inserted space from a real one.
Tab, carriage return and newline are deliberately preserved. toon-go already escapes those correctly, and removing them would corrupt multi-line content.
The copy preserves concrete types, so struct tags survive. That matters more than it looks: TOON field names are part of the contract consuming tools parse, and flattening a struct to map[string]any would rename every column to its Go identifier. Strings inside unexported fields cannot be reached by reflection and are carried through as-is.
The only error returned is *KeyCollisionError. Callers whose keys are fixed identifiers can rule that out and use MustSanitize.
func SanitizeString ¶
SanitizeString cleans a single string using the same rules as Sanitize. It is exported for callers that build output a field at a time rather than handing over a whole value. It cannot collide, so it returns no error.
func WriteHelp ¶
WriteHelp emits an AXI contextual-disclosure block: next-step command templates with fixed flags carried forward and runtime values left as placeholders like <id>.
The form is the inline TOON array, `help[2]: first,second`, chosen because it is the only one of the three in circulation that survives its own codec. The AXI specification's own indented example fails to decode with "list length mismatch", and cadence's `help[] line` form fails with "missing colon after key". Appending this after a body is safe: the combined payload decodes as one value carrying both.
An empty list writes nothing. A command with no meaningful next step should not emit a stray "help[0]:" that an agent pays tokens to read and learns nothing from.
Types ¶
type ExitCode ¶
type ExitCode int
ExitCode is the process exit status an agent-facing CLI returns.
These ship as constants rather than as a rule in a style guide because a documented convention drifts and a compiled constant cannot. The values are cadence-axi's set, which is a superset of atcr's 0/1/2.
const ( // ExitOK is success. ExitOK ExitCode = 0 // ExitError is a runtime failure inside the tool itself. ExitError ExitCode = 1 // ExitUsage is a malformed invocation: an unknown subcommand or flag. // // AXI requires an unknown flag to fail loudly rather than be ignored. An // agent that invents a flag has to learn it did nothing, or it will trust // output that was never scoped the way it asked. ExitUsage ExitCode = 2 // ExitNotFound is a path or item that does not exist. ExitNotFound ExitCode = 3 // ExitValidation means the command ran correctly and the thing it checked // did not pass. // // This is deliberately distinct from ExitError, and it is the load-bearing // distinction in the set. A checker reporting "this does not conform" is a // successful run with a real result, not a broken tool. Collapsing the two // makes "the tool crashed" indistinguishable from "your input is wrong", // and an agent cannot tell whether retrying is worth anything. ExitValidation ExitCode = 4 )
type KeyCollisionError ¶
type KeyCollisionError struct {
Cleaned any // the key both inputs produced
First any // the key already present
Second any // the key that would have overwritten it
}
KeyCollisionError reports two map keys that became identical after cleaning.
This is the one case where sanitizing cannot proceed without losing data. "na\x1bme" and "name" are different keys on input and the same key after the control byte is removed, so one would overwrite the other. Go randomizes map iteration order, which makes the survivor vary between runs — the worst kind of loss, because a test can pass today and fail tomorrow with no code change.
Sanitize refuses rather than picking. Renaming one key would invent data the caller never wrote, and dropping one is the silent loss this module exists to prevent.
func (*KeyCollisionError) Error ¶
func (e *KeyCollisionError) Error() string
type Tier ¶
type Tier int
Tier describes the payload shape TOON encoding actually produces.
The tiers are defined by measured behavior, not by what a format "should" handle. toon-go encodes ragged rows, list-valued fields and nested objects losslessly using a list form, so "can it be encoded" separates nothing useful. What differs is the shape emitted, and whether anything is lost.
const ( // TierTabular is TOON's best case: uniform rows that encode as a tabular // array with a declared field list. This is where the token savings come // from. TierTabular Tier = iota // TierNested is lossless but not tabular. Ragged rows, list-valued fields // and nested objects land here. The payload is correct; it just does not // get the columnar win. TierNested // TierLossy means encoding silently discards data. Nothing in this tier // should be emitted as TOON. TierLossy )
type Verdict ¶
type Verdict struct {
// OK reports that encoding this value as TOON preserves its data.
OK bool
// Tier is the shape TOON encoding produces.
Tier Tier
// Efficient reports that the TOON payload is no larger than the JSON one.
Efficient bool
// Reason explains a false OK or Efficient in terms an operator can act on.
Reason string
}
Verdict is the result of inspecting a value before encoding it.
OK and Efficient are deliberately separate. Correctness and cost are orthogonal: a payload can be perfectly lossless and still cost more as TOON than as JSON, which is a reason to choose JSON for that command but never a reason to call the value broken.
func Check ¶
Check reports whether v can be encoded as TOON without losing data, what shape the payload takes, and whether TOON actually costs less than JSON.
It exists because toon-go fails in ways neither an error check nor a length check catches, all confirmed against the pinned version:
- A defined string type (type Kind string) returns a real error. Loud.
- A TextMarshaler at top level returns a nil error with EMPTY output, so the command prints nothing and exits zero.
- A TextMarshaler nested in a struct or map encodes to "m:" — key present, value gone, nil error, non-zero length. A guard testing only err and len(b) passes this while the data is lost.
Loss is found by reflection rather than by round-tripping and comparing against the JSON projection. That comparison looks correct and is not: toon-go does not fall back to the json tag, so a field tagged only `json:"x"` encodes under its Go field name and the two projections disagree for reasons that have nothing to do with data loss.