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 an agent-facing CLI has to get right before it prints:
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. EncodeChecked applies that guard on the way out in a single pass, for callers that are about to print.
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 CycleError
- 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.
This is runtime format detection. A project that fixes the output format per command at design time should keep its build-time guard and not call this. It exists for callers that have no such rule.
func MustSanitize ¶
MustSanitize is Sanitize for callers whose map keys are fixed identifiers and whose shapes are acyclic, where both failures are impossible by construction. It panics if one occurs, because continuing would mean emitting a payload with a field missing.
A panic is the lesser evil for a cycle specifically: it unwinds and can be recovered, whereas the stack overflow it replaces is fatal and cannot.
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.
Two errors are returned: *CycleError for a self-referential value, and *KeyCollisionError for two map keys that clean to the same string. A caller whose keys are fixed identifiers and whose shapes are acyclic can rule both 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 the `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 CycleError ¶ added in v0.1.1
type CycleError struct {
Type string // the type the cycle was detected in
}
CycleError reports a value that refers to itself, directly or through other nodes.
TOON has no way to express a back-reference, so there is no correct encoding to fall back to. The walk is refused before it is entered rather than bounded part way in: sanitizeValue recurses through pointers with no seen-set, and the resulting stack overflow is a fatal runtime error, not a panic. No recover() catches it, so the process dies with a goroutine dump instead of returning to the caller — the exact class of failure this module exists to convert into an error.
Check already refuses a cycle for this reason. Sanitize now agrees, so the guarantee does not depend on which entry point a caller happened to reach for.
func (*CycleError) Error ¶ added in v0.1.1
func (e *CycleError) Error() string
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 set extends the conventional 0/1/2 with two codes agent-facing tools need to distinguish.
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.
// Only meaningful when SizeCompared is true.
Efficient bool
// SizeCompared reports whether Efficient was actually measured. The
// comparison needs a second marshal, so a path that writes the payload and
// never reads the cost signal skips it. Without this flag a skipped
// comparison is indistinguishable from a measured "TOON is larger".
SizeCompared 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.
func CheckSanitized ¶ added in v0.1.2
CheckSanitized is Check for a value that has already been sanitized, so a caller holding the cleaned copy does not pay to sanitize it twice.
v supplies the declared types for the loss walks; clean is what gets measured. Passing both is deliberate — sanitizing preserves concrete types, so the walks give the same answer either way, but the measurements must be taken on what will actually be emitted.
func EncodeChecked ¶ added in v0.1.4
EncodeChecked sanitizes v, verifies TOON can carry it losslessly, and writes it — one sanitize walk and one TOON marshal serving both jobs.
Use this instead of Check followed by Encode. That pair sanitizes and marshals the same value twice to serve a single guard. Measured medians on the same payload, against a bare Encode as the floor:
rows Encode EncodeChecked Check+Encode 100 134us 156us (+16%) 338us (+152%) 2000 2.91ms 3.32ms (+14%) 7.18ms (+147%)
Allocations at 2000 rows: 98,073 / 110,099 / 222,204. The guard costs about 14%; the rest was duplicated work.
Nothing is written unless the verdict is OK, because a partial payload is worse than none — it parses. The returned Verdict describes the bytes actually written, so Tier is measured rather than predicted.
Efficient is deliberately NOT measured; SizeCompared reports that. The comparison needs a second marshal that a caller writing the payload does not read. Call Check when you want the cost signal.