Agent-First Data v0.26: One Surface, Three Formats

by Agent-First Kit Contributors

`--output` selects a format. It was also selecting a different set of commands and arguments: Markdown help was the one format that bypassed the help model and handed each command back to the argument parser's own writer, so `afdata lint` listed two arguments in JSON and plain and three in Markdown, and all 23 commands re-advertised a `-h` flag removed two releases earlier. v0.26 renders Markdown from the same model the JSON help uses, so the formats cannot disagree by construction, and puts a gate check behind it. v0.26.1 fixes the mirror image in `afdata lint`, which read a JSON Schema as though it were data — and so rejected `duration_ms: {"type": "integer"}`, a property that satisfies the very suffix rule the lint exists to enforce.

v0.24 made help a normal result. v0.25 removed the second spellings around it. Both releases assumed something that turned out not to be true: that --output was only choosing how the help model gets rendered.

For one format, it was choosing a different model.

A format selector that changed the answer

afdata lint --help --output json      # two arguments
afdata lint --help --output markdown  # three

Same command, same scope, two different surfaces. Markdown was the only format that never reached the help model — it took each command straight to Clap’s own help writer and wrapped the rendered terminal text in a fenced block. Whatever the model decided, Markdown could not inherit it.

So it kept its own history. It advertised -h for a year after that flag was removed. It repeated --help on all 23 commands, where the JSON model lists an inherited global once at the root. And where the model had corrected an argument list, Markdown still described the uncorrected one.

A format is a rendering. The moment it can disagree about what exists, it is not a format any more — it is a second implementation with a much quieter test suite.

Markdown that is actually Markdown

Markdown help now renders from command_arguments_schema, the same function that builds the JSON model. Not “kept in sync with” — the same function, so the two cannot drift apart without one of them changing.

The export stopped being a pile of terminal output while it was there. Each command gets a heading, its prose, a usage block, and tables for arguments and subcommands — real Markdown structure instead of 23 concatenated 80-column dumps. docs/cli.md came out 205 lines shorter and carrying more information than it had before.

One case still renders through Clap, deliberately: one-level plain help. That is the terminal convention, and with no descendants listed there is no surface to diverge about.

The gate that keeps them honest

Agreement that depends on nobody touching the renderer is not agreement. A new check runs in the gate: at a single scope, json, plain, and markdown must name the same commands and the same arguments.

help formats agree: 23 commands across json, plain, markdown

Prose may still differ, on purpose — Markdown keeps the long-form sections the compact model drops. What may not differ is the surface. The check was verified the only way worth trusting: by reintroducing the old renderer and confirming it fails.

-0 had to go

--null on paths and keys had a short alias, -0. It is gone, by the same rule that retired --json in v0.25: a short flag earns its place when it imitates an established interface, never when it merely abbreviates a name we invented ourselves. Two spellings, one meaning, and an agent does not count characters.

--null itself is now documented where a reader actually meets it — the Bash authoring guide and the README — including the part that matters:

afdata paths config.json --null | xargs -0 -n1 echo

The default line-separated output is not safe for arbitrary documents. A JSON key may contain a newline; NUL is the one byte it cannot contain. That was true before, and undocumented, which is the worst combination for a flag whose whole job is correctness under hostile input.

v0.26.1: a schema is not the data it describes

The same class of confusion was sitting in afdata lint.

Lint accepts a JSON Schema as input. But its suffix type check read a properties map as though it were runtime data — and a schema descriptor is an object, while _ms demands a number:

{"type": "object", "properties": {"duration_ms": {"type": "integer"}}}

Every correctly named, correctly typed property in that schema came back as suffix_type_mismatch. The lint that exists to enforce the naming convention rejected the documents that follow it.

A properties map is now read as a property map: the key is a field name, the value is that field’s schema, and what gets checked is the type it declares — through type arrays, anyOf/oneOf (any branch), and allOf (every branch).

{"duration_ms": {"type": ["integer", "null"]}}   // passes
{"duration_ms": {"type": "string"}}              // suffix_type_mismatch

Where a schema constrains no primitive type at that location — a $ref, a bare const, a boolean schema — lint says nothing rather than guessing. The value-level checks have no schema counterpart either: a _rfc3339 property need only declare a string, because a schema carries no instant whose RFC 3339 structure could be checked.

The tempting fix was to skip properties subtrees entirely, and it would have traded a false positive for a blind spot — a config document that happens to name a field properties would quietly stop being linted. JSON Schema draws the line for us: a schema is always an object or a boolean. So an entry holding a scalar or an array is data, not a schema, and keeps the runtime check:

{"properties": {"timeout_ms": "5000"}}   // still a mismatch

Where it lands

Both halves of v0.26 are one mistake in two places: reading something as a kind of thing it is not. Markdown help described a surface the model had already reshaped. Lint checked a description as though it were the thing described.

Neither is new capability. Both close a gap between what AFDATA says about itself and what it actually does — which, for a convention whose entire premise is that a name tells you the truth about a value, is the only kind of gap that really matters.