Repository navigation
Releases
gnr8 0.21.0
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.21.0 — 2026-10-22
Breaking
OpenApi31,OpenApi31Json,GoSdk,PySdkandTsSdkhave a new public
excluded_operationsfield, so a struct literal of one of them must set it. Build them with
new()and the builder methods instead.
Added
OpenApi31,OpenApi31Json,GoSdk,PySdkandTsSdkaccept.exclude(OperationSelector).
The target leaves the matched operations out of its own output, with their per-operation policies
and every schema only they reach; sibling targets still emit them. A published reference and a
first-party client can come from one pipeline. A selector that matches no operation is a
configuration error, andStaticDocsrefuses a sibling SDK that excludes operations.
gnr8 0.20.0
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.20.0 — 2026-10-19
Breaking
- Committed graph artifacts use schema version 3: provenance keeps the source file, and the graph
retains its existing symbol identities. Source line spans and diagnostic line numbers are
omitted.gnr8 changesupgrades schema-v1 and schema-v2 base artifacts before comparison and
reports source locations from the current run. Live diagnostics retain lines. - Generated SDK READMEs no longer include aggregate operation and schema counters. Published
diagnostics show file paths only and sort by file, code, message and symbol, with identical
rendered diagnostics deduplicated. OpenAPI YAML enums and required fields use one block-sequence
item per line, preserving value order. These outputs no longer change when source lines move.
Fixed
- OpenAPI YAML quotes strings a YAML reader would otherwise resolve to a number or a merge: radix
integers (0x1F,0o17,0b101), underscore-grouped digits (1_000) and<<, whether they
appear as enum members, required fields or schema keys.
gnr8 0.19.0
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.19.0 — 2026-10-12
Breaking
-
Go extraction no longer publishes a request null that a custom decoder decides.
encoding/json's rules say whether a field accepts an explicitnullonly for the null it
decodes itself. gnr8 used them for every field, so a field whoseUnmarshalJSONrefuses null was
published as nullable, and the SDKs offered a null the server rejects. A field is now published as
not accepting null, and raises the newWARNdiagnosticschema.nullability.unresolved, when its
null goes to code gnr8 does not read:- every field of a type that implements
json.Unmarshaler, its own method or one promoted from an
embedded struct; - every field of a type decoded inside such a type's value, at any depth, whatever else decodes
it; - a value field whose own type implements
json.Unmarshaler(a pointer field is not:encoding/json
sets it to nil without calling the pointee's method, and neither istime.Time); - a
binding:"required"/validate:"required"json.RawMessage, which keeps a literal null as the
bytesnullthatrequiredpasses. A required map was already not nullable, sincerequired
refuses its nil.
The diagnostic names the type and the JSON field and is raised only where
encoding/json's rules
would have published the null; itsschemais the schema id and itssubjectthe JSON name. It
counts only while its schema publishes an input answer: aDiagnosticPolicyasks of the graph it
runs on, and the run reports it only if the finished graph still does, so it is dropped when only
responses reach the schema or a transform removes it. It stays through every transform, so a later
register_input_schemaor request body brings it back into scope, and it follows aRenameType
to the new name. State the answer in.gnr8with
ApiOverrides::force_nullable(schema, field, SchemaUse::Input)orforce_non_nullablewith the same
arguments: on such a field either one is accepted, neither is redundant, and the diagnostic is
retired. ADiagnosticPolicythat denies the code fails the pipeline on every null left unstated.
There is no struct tag for this fact. A field that is no longer nullable keeps 0.18.1's rule: a
PythonNonein an optional field is the absent key, so the SDK never sends the null.
openapi.yamldropsnullfrom each such property, and every generated model changes for it as
for any field that stops accepting null: TypeScript drops| null, a required Python field drops
Optional[…]and an optional one defaults toNoneinstead ofUNSET. Regenerate, then declare
each field the diagnostic names. - every field of a type that implements
gnr8 0.18.3
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.18.3 — 2026-10-12
Fixed
- Generated CLI body help preserves field-level enum restrictions and request nullability, quotes
ambiguous enum labels, and distinguishes container element unions from nullable containers.
Body help no longer recalculates whole-graph directions for every command. - OpenAPI output intersects inline enums with field constraints, retains empty enum sets, and
includes null in nullable enums. In 3.1enumconstrains every value, so a nullable property's
member list withoutnullrejected the null it admits. Consumers that readenummembers as
strings must now skipnull. A constraint narrows an inline enum without discarding the
member orderSetEnumOrdergave it. - Fixed the generated CLI documentation's Body example closing fence.
- Go extraction reports a field-scope
oneofrule on a slice or map field as
schema.metadata.unresolvedinstead of publishing anenumon the collection itself, which no
non-null value satisfied (the validator panics on that rule). Aoneofpastdiveand the element
type's own values are unaffected. - OpenAPI import reads a
{type: 'null'}arm ofoneOf/anyOfas nullability. The 3.1
nullable-$refform gnr8 emits now imports as the referenced type, nullable, instead of a union
with an untyped member that the Go SDK rejected. A$refto a component written that way is
nullable too. - OpenAPI YAML quotes
,,[,],{and}anywhere in a flow-sequence item (enum members,
tags,required, scopes), so a member such asa, bno longer splits in two.
gnr8 0.18.2
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.18.2 — 2026-10-11
Added
- Retired flags. A
CliRenameErrormay end in one long flag the command no longer takes:
CliRenameError::new(["books", "get", "--book-id"], "books get <id>").books get --book-id 1
printsbooks get --book-id is now bookstore books get <id>, exits 2 and sends no request, in
the Go and Python CLIs. Generation refuses a retired flag that the command binds, one after a
topic or with no command, one after a path that extends a generated command,--help,-hand
--version, and (Python) one that begins a bound flag, because argparse reads--debas
--debug. checkRetiredfor hand-owned Go commands. Generatedpackage clialways declares
checkRetired(path, args []string) int. A hand-owned dispatcher calls it before it reports an
unknown command, so the declared table also covers retired paths under hand-owned commands.
Only a retired invocation beyondpathmatches.parseFlagscalls it with the flag tokens when
a parse fails, and reads the command path from theflag.FlagSetname, so it resolves only
retired flags of that path. Generation now accepts retired invocations under root and topic
owned commands.
Changed
- Retired invocations resolve on the error path. The generated CLIs no longer match retired
paths before dispatch. The Go CLI looks up the table only for an unknown command under the root,
a topic or a sub-noun, an unexpected argument after a generated command that takes none, and a
failed flag parse; the Python CLI only when argparse rejects the arguments. A retired invocation
therefore never shadows a live command. When several match, the longest command path wins, then
a retired flag over the path it follows, then the retired flag typed first. Order no longer
matters, so generation accepts a retired path under a shorter one and refuses only a
duplicate. A retired path that extends a generated command without arguments now yields to that
command's--help. The message, exit code and output channel do not change. Generated Go names
each command'sflag.FlagSetafter its full invocation (books get), andcheckRetired
replaces the undocumentedcheckRename.
gnr8 0.18.1
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.18.1 — 2026-10-11
Breaking
BuiltinTargetgainsStaticDocs. Rust code that matchesBuiltinTargetexhaustively needs
an arm. The host/worker protocol is now version 9, so a worker and CLI cannot silently disagree
about the stage-plan shape.gnr8::facts::Constraintsgainsmultiple_ofandunique_items, andgnr8::graph::Param
gainsexample. Code that builds either struct with a literal, anOpenApiFieldPatch's
constraintsincluded, must set the new fields or finish with..Default::default().- Go extraction no longer reads
enums:"…"/enum:"…"struct tags. That spelling belongs to
another generator, and no Go runtime consumes it. A field or bound parameter that stated its enum
only that way now publishes a plain string, with no enum and no diagnostic. State the enum with
the validator's own rule instead:binding:"oneof=a b"orvalidate:"oneof=a b". Separate the
members with spaces. That rule is the one source of an enum constraint. generated/gnr8.graph.jsonis schema version 2. An imported parameter's validation keywords
now live in its typed constraints instead of the raw schema the graph keeps, its scalar
examplemoves into the newParam::example,Constraintscarriesmultiple_ofand
unique_items, and an imported server no longer repeats the base path. Tools that read the
artifact must accept version 2.gnr8 changes --base <ref>still reads a version 1 base, by the
importer's own rules, so the first comparison after upgrading reports no change the API did not
make. The rules below that leave a fact unknown apply only to what the base imported from an
OpenAPI document: a node whose provenance file is a JSON or YAML document (openapi.yaml), or
for servers, a base with such a node. Anything gnr8 extracted from source code is compared whole,
so a fact added to it is reported:- A parameter is read through the importer's parameter rules. 0.18.0 did not import a Swagger 2
parameter'smultipleOf, so an imported parameter'smultipleOfis unknown when the base
states none; one the base states is compared. A field bound kept as the OpenAPI
3.0 flag (minimum: 0withexclusiveMinimum: "true", in a schema, a parameter's type or a
response header's type) reads as the exclusive bound. - A fact a version 1 artifact could not hold of an imported field is unknown, not added:
multipleOfanduniqueItems, an enum or an example the base does not state, and a plain
string that is now a string enum. Every fact the base states is still compared. So on that
first comparison, an enum or example newly added to an imported field whose base had none is
not reported. - An imported field that names the same schema as before with
$refand now accepts null is
unknown, not changed: 0.18.0 dropped the null such a schema admits (see Fixed). - A version 1 artifact never held the bounds of the schema an imported parameter names with
$ref. A keyword the current graph holds tighter than the base states reads as the base's
value, and so does an enum that is a subset of the base's; a keyword loosened or removed is
still reported,patternis compared as it is, andmultipleOfas any imported parameter's
is (above). So on that first
comparison, a real tightening stated beside the$refis not reported. - An imported base server whose URL is a current server's URL followed by the base graph's base
path (https://api.example.com/v1beside/v1, trailing slashes ignored) is the same server.
A server set withOpenApiMetadata::serverin a graph read from source code was sent as
written in both versions, so moving it off the base path is reported.
- A parameter is read through the importer's parameter rules. 0.18.0 did not import a Swagger 2
- A Python field that is optional and nullable can be sent as an explicit
nullagain, the
PATCH that clears a value. A dataclass field of that kind is typedUnion[T, Unset]and defaults
toUNSET, defined in the newunset.pyevery dataclass SDK carries (from <package>.unset import UNSET; falsy, with__bool__typedLiteral[False]so a type checker narrows on it,
andUNSETas bothreprandstr).to_dictsendsUNSETas no key andNoneasnull, and
from_dictreads an absent key asUNSETand anullasNone, where both used to readNone.
Code that tests such a field withis Noneafter decoding a reply must testis UNSETfor an
absent key. The generated CLI printsUNSETasnull. A Pydantic model's field still reads
None, andto_dictsendsnullwhen the field was set (model_fields_set) and omits it
otherwise. An optional field that is not nullable is unchanged: itsNoneis the absent key.
Generated dataclassmodels.pyandcontract_test.pychange for such fields, the generated CLI's
output.pychanges in every dataclass SDK with an object schema (it importsUNSET), and
Pydanticmodels.pychanges for every model with one. - Each SDK's
README.mdandreference.mdare rewritten from the docs model thatStaticDocs
renders its pages from, built for that one SDK.README.mdreplaces thePackage/moduleline
withImport(the Go module path, the Python import package, thepackage.jsonname) and
Install(go get,pip installwith the distribution namepyproject.tomllists,npm install), and its<Language> quick startplaceholder with## Quick start: the first sampled
operation's call, byte for byte asreference.mdprints it, or, when none has one, a line saying
that no operation has a sample call and thatreference.mdsays why under each operation. An SDK with no package metadata
says why it has no import and no quick start. Its Agent workflow's first step now points to
reference.mdfor every operation, schema, error and credential, each with its sample.
reference.mdis one file holding every docs page as a section, one heading level down: the
index (which lists every operation and schema, and the Errors and Authentication sections by
name), each operation (parameters, bodies, responses, the call, typed-error and iterator samples
for this SDK, the CLI invocation), each schema, the error catalog (The generated SDK surfaces a non-success status as its typed error, …) and authentication. ItsOperations,Operation DocumentationandSchemastables are gone; it links onlyREADME.md, and a diagnostic is
printed under its operation, or under the index section when it names none. The API title is
printed as itself in both files, with any Markdown syntax in it escaped. Writing these files now
builds the docs model, so with docs on (the default) an SDK target fails generation on what
StaticDocsfails on —CoreError::InvalidExamplefor an invalid declared example,
CoreError::DocsGenfor a blank API title or group name or prose that breaksreference.md's
structure — and refuses anOpenApiSchemaPatchthat changes a field factreference.mdprints
(see the next entry)..without_docs()writes neither file. Tools that parse either file must
follow the new layout. - An existing
OpenApi31schema patch that sets a documented field fact now fails generation
in any pipeline with an SDK target that writes its docs (the default) or aStaticDocstarget: a
constraint (multipleOfanduniqueItemsincluded), enum, description, default or example set
by anOpenApiFieldPatchis a configuration error naming the docs output, the target, the field
and the facts, because the docs and their samples read the graph and would disagree with the
published spec. Set the fact in the source or with aTransformthat edits the field, or declare
each SDK.without_docs(). A patch that only addsx-*extensions is unaffected. gnr8 verifychecks the samples an SDK'sREADME.mdandreference.mdprint, whether or
not the pipeline declaresStaticDocs: an SDK target that writes docs and has a package manifest
gains a docs suite (<Language> docs samples), which compiles and runs each sample and fails when
either file no longer prints it. A target with neither contract tests nor docs leavesverify
nothing to check, as before. With nothing to check,verifynow saysno SDK contract tests or generated CLI help checks to run, and no docs samples — add an SDK, CLI or StaticDocs target to .gnr8/src/main.rs, and a run in which every suite skipped saysno checks executed: every generated check was skipped.- Declared examples are the sample, and an invalid one stops generation. A field's
example,
a parameter'sexample, and an operation's first request or responseMediaExamplefor the JSON
media type the sample uses, are now the values the docs pages and the contract tests send and
reply with. A body or reply that declares an example is that example, and is not built from its
fields. Each declared example is checked against its input before anything is sampled, whether a
sample uses it or not: its type (an integer within its type's width and sign, auint64above
i64::MAXincluded, a float32 within the float32 range), the required fields, fields the schema does not declare, nullability and
every constraint exceptpattern. An integral number is an integer: a declared5.0is accepted
for an integer input and stated as5. A field example is read as a value of the field's type,
so it can state only a scalar:example:"a,b"on a[]stringfield is not a list, and an
imported array, object ornullproperty example is reported as a diagnostic and not imported.
An example that breaks its input fails generation withCoreError::InvalidExample, which names
the file and place the example is declared in and the type, range or constraint it breaks
(`example...
gnr8 0.18.0
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.18.0 — 2026-10-10
Breaking
CliTopicgainsowned_commands. Older serialized configs remain readable because the field
defaults to empty, but Rust code that constructsCliTopicwith a struct literal, or matches it
exhaustively, must account for it.- Generation refuses CLI configs that produced a broken CLI. Two owned commands that call one
Go function, anOwnedCommand::functionthe generated CLI already uses, and arename_errorthat
reaches a live command are now generation errors (see Fixed). A config that generated with any of
them must change.
Added
- Hand-owned commands inside generated topics.
CliTopic::owned_commandnames a Go command
under a declared topic whose implementation is hand-owned. The topic's dispatcher calls
run<Topic><Name>(orOwnedCommand::function), and its help page and typo hints list it first.
Generation refuses a topic with no generated command, a name equal to one of the topic's verbs or
sub-nouns, and topic owned commands onPySdk. Configs without the new field emit identical
output.
Fixed
OwnedCommand::functionmust be an ASCII Go identifier. The dispatcher emits the function
name as Go source, so generation now refuses a value that is not an identifier, is a keyword, or
uses non-ASCII letters (gnr8 emits ASCII identifiers only), at the root and under a topic.OwnedCommand::functionmust not be a name the generated CLI already uses. A declared name
(complete,Run), an imported package (strings,json), a Go predeclared name (len,
string),_orinitcalled gnr8's own code, shadowed it, or was not callable, socli.godid
not compile; generation now refuses it.- Two owned commands may not call one Go function. Root
db-typesand topicdbcommandtypes
both deriverunDbTypes; generation now refuses that and an explicit shared function. rename_errormust not reach a live command. A retired path is matched before dispatch, so a
path that was a live command or a prefix of one (books,help) made that command unreachable, a
flag token (--help) hid help or version output, and one that extended a command taking arguments
captured them. Generation now refuses these, and a retired path that an earlier one already
matches, in the Go and Python CLIs.
gnr8 0.17.2
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.17.2 — 2026-10-10
Added
- Generated Go CLI
--helpprints a Body section after Flags on every command that takes--body:
one row per request-body field with its JSON type,required/optional,one of:values for an
enum, and its description cut to 80 characters. Top-level objects and arrays of objects list their
fields one level deep asparent.child/parent[].child, a repeated or recursive shape names the
path that already lists it, and fields bound as flags throughbody_fieldsare left out. help --jsoncarriesbody: {schema, fields}for each command that takes--body, in generated
Go and Python CLIs.
gnr8 0.17.1
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.17.1 — 2026-10-07
Added
gnr8 verifychecks root, group, and every selected operation command's--helpfor generated
Go and Python CLIs, with explicit skipped-tool reports.
Fixed
- Go extraction carries supported validation bounds on named scalar and collection fields while
preserving their schema references. - OpenAPI YAML quotes the schema type name 'null', so nullable reference and union arms remain
string-valued schema types when parsed.
gnr8 0.17.0
Install with the archive assets below, or run: curl -fsSL https://raw.githubusercontent.com/oaiz-io/gnr8/main/scripts/install.sh | bash.
0.17.0 — 2026-10-02
Added
- Parameter descriptions are a graph fact. A bound parameter's prose is the field's own doc
comment (plain prose, no tag grammar), the imported spec'sdescription, or
DocumentOperation::parameterwhen the source has neither. A collision is a hard error. Generated
CLI--helpprints that prose on the flag. - Generated CLI output format flags.
--format human|ai-friendly|json|jsonl,--jsonas the
JSON shorthand, and{PROG}_FORMAT. A TTY defaults tohuman; anything else defaults to
ai-friendly.--fields,-o/--output,-q/--quiet,--debug,-y/--yesand--no-input
are reserved globals. Exit codes 0–6 name the caller's next action; transient HTTP statuses, a failed connection or a
timeout are 6 ("retry later"), so a success body that does not decode or a failed-owrite is 1.
Python returns 130 on Ctrl-C. Errors printerror:plus optional hints and a request id; under
--jsonthey are one object on stderr carrying the server'sslug. - ai-friendly result envelopes. Off-TTY default output is a bounded summary whose first line
names a versioned envelope written under.{program}/output/(or{PROG}_OUTPUT_DIR): atomic
0600 files, a.gitignore, a 100-file / 100 MB retention cap over envelopes and downloads that
never deletes the files the current run wrote, and a preflight before the request. The summary
reserves room for the next-page line andjqrecipes; whether a result is a list is decided at
generation time from the graph.--jsonprints the captured server body. - Go CLI library seam.
Run(args, Options)is the exported entry.main.gostamps
version/commit/dateas variables so-ldflags -Xcan overwrite them.
SdkCli::hand_owned_main()skips that file;SdkCli::owned_commandnames a root command whose
implementation is hand-owned and never generated. Python rejects both — they are a Go seam. - Declared CLI command spec.
SdkCli::topicnames verbs, sub-nouns, positional identifiers,
body-field flags, switch flags,@vN/@latestselectors, and confirmation severity.
rename_errorprints the replacement and exits 2 without sending a request.viewnames the
preview fields for the rows of one schema, in declared order. Absent a spec, command names stay
the kebab-case operation ids. - Paginated commands emit
--cursorand merged page JSON.--limit/--allcollect items
into{itemsKey: […], "hasMore": …}instead of a bare array, so--jsonis not the last page's
body.--page-sizeis a rename error naming--limit. AConfigurePaginationcursor policy
seeds--cursoronto the request. - Help layout and
help --json. A declared command spec prints Arguments, Examples, Output,
See also, and Docs on--help. Generation fails when a spec command has no example.
help --jsonprints the command spec, including each flag's type and enum members. - Human polish and shell completion.
--color auto|always|never(withNO_COLOR/TERM=dumb),
width truncation,{PROG}_PAGER/PAGERpaging of long human TTY output, and a stderr progress
line on paginated walks.completion bash|zsh|fish|powershellprints a script; hidden
__completeanswers static candidates from the spec plus live identifiers (one list call, 1s
timeout).
Fixed
- CLI shell recipes quote paths and cursors as single arguments, including shell metacharacters.
- Repeated identical results keep separate envelopes. Failed atomic writes remove their temporary
files, and binary downloads preserve their bytes and file envelopes. - Switch flags dispatch in both languages, validate collisions and operation inputs before emission,
and use the alternate operation's Go parameter type and result shape. Paginated switches honor
limits, retain server metadata, request the remaining size, and preserve only boundary cursors. - Each generated CLI ships its versioned result JSON Schema, checked against persisted envelopes.
- Human errors retain the request id within six lines, including when the server sends many hints
or embedded line breaks. - Machine help includes path, body, switch, and global flag types. JSON usage and authentication
errors use the structured error renderer, and one large preview row cannot exceed the byte budget. --fieldsprojects JSON items while preserving page metadata and large integer values;-o -
prints the complete response without an envelope.
Changed
- Output is no longer unconditionally JSON.
--formatand--jsonare reserved globals; a
parameter namedjsonorformatis a generation error, and so is one namedq,oory
(the short spellings of--quiet,--outputand--yes). - Per-flag prose is emitted. A parameter's description is the one graph fact
--helpprints for
it.SdkCliis no longer a three-field type: it carries the program name, command scope, host,
library seam, and the declared command spec.