State as of 2026-04-30 (Discord blockers cleared)
What this is
space_gen is iterating toward a "fantastic" Dart OpenAPI generator.
The methodology: run real-world specs through it, see what breaks
or reads ugly, fix one thing per PR. Each PR should leave every
tracked spec at least as good as it was — ideally better.
Validation targets (in ~/Documents/GitHub/personal/gen_tests/,
parallel to the repo, not checked in):
- github —
api.github.com.json, ~1000 operations, the primary
punching bag. Exercises everything (oneOf, discriminators,
multi-status responses, big enums, recursion, status code ranges,
complex error handling). Currently dart analyze clean on the
regen with all 6100+ generated round-trip tests passing — the
bar to maintain.
- Discord —
discord.json, ~511 schemas / 141 paths. OpenAPI
3.1. Snowflake IDs are type: string with a numeric pattern (so
no int64 wire-format trap), but exposes patterns github doesn't:
bitfield-style integer enums (132 sites, closed by #192), and
type: string newtype schemas with regex patterns (327
validatePattern call sites, closed by #194). Both blockers
cleared as of 2026-04-30 — Discord regen progresses well past
the parser-throw and the validation-call-site bug. Remaining
failures are different gaps surfaced by the now-deeper traversal.
- petstore —
petstore.json, the OpenAPI canonical example.
Smoke test for the basics.
- spacetraders —
spacetraders.json. Smaller real spec.
- train-travel —
train-travel.yaml. YAML-format coverage.
There's also private_gen_tests/watchcrunch-api.json — a user-
reported spec used as a third-party validation target (not Eric's
own service).
A "gap" can be any of:
UnimplementedError stub in generated code (always wrong).
- Generated Dart that doesn't compile, doesn't analyze clean, or
fails its own round-trip tests.
- Output that's correct but ugly (double-prefix names, redundant
?, fake oneOf wrappers, blank lines, etc.).
- Missing OpenAPI feature (cookie params, multipart edge cases,
spec-side
nullable: true quirks, etc.).
- A lint that fires on the generated code (
comment_references,
prefer_single_quotes, lines_longer_than_80_chars).
- A real-spec author hits a snag and reports it.
Github status: dart analyze clean on the regen. 3 stubs
remain (Group C only — the enumless anyOfs the skill suggests
skipping). Stub Groups A (validation-error twins) and B (labels
requestBody) closed in PRs #177 and #183. Generated test suite:
6100+ tests, all passing — 41 round-trip-correctness bugs fixed
in #184. Smoosh series complete: #168–#170 (predicate /
discriminator / shape+hybrid dispatch), #179 (exclusive-by-use
top-level $refs), #189 (inline allOf — RepositoryRuleDetailed's
21 wrappers folded into the sealed parent). Naming and dispatch
arcs settled; structural dedup of operation-synthesized oneOfs
landed in #180.
Discord status: Both pre-existing parser/render blockers are
now closed:
- Integer enums (132 sites) — closed by #192. Generalized
SchemaEnum.enumValues from List<String> to a typed family
(SchemaStringEnum / SchemaIntegerEnum) mirroring the existing
SchemaNumeric<T> split. Composes with the multi-value-enum
implicit discriminator picker from #182.
- Pattern validation on newtype Strings (327 call sites) —
closed by #194, taking the principled route. Validation now
lives in the newtype's own constructor (validate once at
construction) rather than at every API call site. Required
making synthesized example values actually satisfy their schema
(regex-aware candidate testing for strings, range/multipleOf-
aware for numerics) so the auto-generated round-trip tests
don't throw
ArgumentError from const Foo('example') against
a regex newtype.
The next thing to do with Discord is regen and assess — the
deeper traversal will surface gaps that the parser-throw was
masking. Run a -v regen and tally the warnings (see "Discovery"
below).
Discord notably does NOT motivate the int64/web design call from
issue #185: their snowflake IDs are JSON strings with a numeric
pattern, not format: int64 — they sidestepped the precision
problem by design. Only 7 int64 sites in the spec total, and one
schema is literally named Int53Type to acknowledge the JS limit.
The picture beyond stubs and visible-output ugliness. A -v
regen surfaces feature gaps the skill historically didn't track.
The github 2026-04-29 baseline:
| Count |
Category |
What |
Status |
| 246 |
Ignoring: format=int64 (String) |
int64 transmitted as JSON string for precision |
tracked in issue #185 (web-aware codegen — needs design) |
| 81 |
Unused: readOnly=true |
response-only fields not marked as such |
tracked in issue #186 (request/response split) |
| 19 |
Unused: x-multi-segment=true |
github vendor extension (path-segment hint) |
known; -v log only — no action needed |
| 12 |
Ignoring: readOnly=… (non-property slots) |
readOnly outside a property — composes with #186 |
tracked in issue #186 |
| 8 |
Unused: readOnly=false |
spec noise — explicit false matches default |
acceptable noise |
| 57 |
<Parent>OneOf<i> wrappers in regen |
naming gap — variants the heuristics don't catch |
open (low priority — see "Naming polish") |
| 6 |
_1 collision suffixes |
snake-name collision resolution |
open (low priority) |
| 3 |
enumless-anyOf stubs |
Group C — skill says skip (try-each is order-dependent) |
parked |
| 2 |
Ignoring: required (List<…>) |
parser drops required: [foo] when foo isn't a real property |
landed in #184; the warn-log is the feature |
| various |
unknown formats |
repo.nwo, timestamp |
open (low value) |
Gen one of these and you'll likely find more than one PR's worth
of work. Mine the verbose log first — see "Discovery" below
under "Open gaps". The naming/smoosh/dispatch arc has settled;
the next high-leverage tracks are issue #185 (web-aware codegen)
and issue #186 (readOnly request/response split) — both need
design passes before code.
Architecture
The pipeline:
Load → Parse → Resolve → Dispatch → Naming → Render
│ │
▼ ▼
sidecar lookups (Decisions, Names)
lib/src/dispatch.dart (#160). Pure-structural pass: for
each ResolvedSchemaCollection, picks a DispatchDecision
(Discriminator / Shape / Hybrid / Predicate / NoDispatch).
Variants are referenced as ResolvedSchema, no Dart names.
Predicate IR (KeyExists, ArrayElementHasKey, Always) —
adding a new predicate is a subtype, not a new mode.
lib/src/naming.dart (#161–#163, #165, #166, #167). The
source of truth for every Dart class name and every file
basename. Single NameAllocator keyed by snake names; camel is
derived via camelFromSnake on read. Each entity submits a
preference list (best/shortest first, longest/safest last);
the allocator runs a fixpoint that advances colliding entities to
their next preference, then numeric-suffix-disambiguates any
remaining ties. Order-independent.
- Two-phase resolution: phase 1 claims schemas + synthesized
op-response names; phase 2 claims wrapper subclass names (which
depend on phase-1 resolved variant names).
AssignedNames
exposes both snakeFor(pointer) and []/maybeGet (camel,
derived).
- Title-derived names (#167): inline oneOf/anyOf variants
with a spec
title: opt into a multi-tier preference list
[<title-snake>, <parent>_one_of_<i>]. When the title is
unique, the variant gets the descriptive name (file + class
paired). github example: org-ruleset-conditions variants
flip to RepositoryNameAndRefName etc.
- Doubling fallback (#165): when a wrapper's
<parent><variantTag> would double the parent prefix (because
the parser-synthesized inline variant name already starts with
the parent), the wrapper falls back to <parent>Variant<i>.
SpecResolver (in render_tree.dart) consults both pre-
passes: decideDispatch(source) per oneOf, and a names= field
populated from assignNames(spec) at the start of toRenderSpec.
Test helpers (renderTestSchema, renderTestOperation, …)
populate names with assignNamesForSchema(...) /
assignNamesForOperation(...).
Strict-mode invariant (#162): RenderSchema.typeName and
RenderOneOf.wrapperTypeName throw StateError if the lookup is
missing. Production crashes loudly if any render path forgets to
populate names. Tests that build schemas directly without going
through SpecResolver must pass assignedName: 'Foo' explicitly.
Resolver stays Dart-blind. It carries snakeName (parser-
level) and resolves snake-name collisions with _1 suffixes; it
knows nothing about Dart class names.
Smoosh (the structural matrix)
For an inline ResolvedObject variant of a oneOf/anyOf, today's
output is smooshed (#168, #169, #170): the variant data class
itself extends the sealed parent, inlined in the parent's .dart
file, with no separate wrapper subclass and no value: shim.
Pattern matching destructures fields directly:
switch (req) {
case ProjectsCreateCardRequestOneOf0(:final note): // direct
print(note);
}
instead of unwrapping value::
case ProjectsCreateCardRequestVariant0(value: final v):
print(v.note); // pre-smoosh
Smoosh applies when:
- Structural eligibility (
_isStructurallySmooshable): variant
is ResolvedObject AND its pointer is a child of the
collection's pointer (so it's inline-exclusive by construction).
ResolvedAllOf is excluded — it collapses to a synthesized
RenderObject at render but isn't a ResolvedObject upstream.
- Render support (
_dispatchEmitsSmooshed): the parent
dispatch's render template emits the smooshed form. Today this
is exhaustive — every DispatchDecision kind capable of smoosh
does it (predicate-required, discriminator, shape, hybrid Map
sub-arms). Array-element predicate is excluded structurally
(variants are arrays, not objects); NoDispatch doesn't claim
wrappers at all.
Sealed subclasses must live in the same library — Dart's sealed
modifier restricts cross-library extension — so smooshed variants
are emitted inline in the parent's .dart file, not their
own. RenderObject.parentSealedTypeName: String? carries the
parent's class name; RenderSchema.isSmooshed is the check used
by the dispatch builders and file_renderer.
Render-side dispatch builders
Each dispatch kind has a _buildXMode(...) helper on RenderOneOf
that splits variants into dispatch (per-arm caseExpressions),
variants (non-smooshed wrapper subclass declarations), and
smooshedVariants (full toTemplateContexts rendered inline via
the schema_object partial). The four builders follow the same
shape:
_buildDiscriminatorMode — switch (json[propertyName]).
_buildShapeMode — switch (json) { T v => ... }.
_buildHybridMode — non-Map shape arms + Map sub-dispatch with
optional unguarded fallback.
_buildPredicateMode — if-chain (required-field or array-
element).
Each caseExpression is the full Dart expression to return from
its switch arm — pre-composed in render code so the template just
splices it. Smooshed: <Variant>.fromJson(<arg>). Non-smooshed:
<Wrapper>(<Variant>.fromJson(<arg>)).
Recent PRs
The 2026-04-28/29 fan-out (12 PRs in one night, 11 landed):
| PR |
What |
| #173 |
closed — replaced by issue #185. Initial framing of format=int64 was github-specific (assumed int was correct on VM, glossed over dart2js precision loss). |
| #174 |
Handle inline oneOf/anyOf at property schema slots (precedence fix: explicit oneOf wins over multi-type-array expansion) |
| #175 |
closed — replaced by issues #186, #187. readOnly doc-comment marker was a half-measure; real fix needs request/response split (#186), and the Equatable plumbing it added is the start of cleanup #187. |
| #176 |
Detail-log spec-author quirks (required on array, maxProperties, etc.); drop the silent vendor-extension filter (x-* shows in -v for discoverability) |
| #177 |
Dispatch validation-error twins via new PropertyArrayItemShape predicate; closes 2 stubs |
| #178 |
Surface spec example: values in generated doc comments (51 sites) |
| #179 |
Smoosh top-level $ref variants used by exactly one parent (RepositoryRule family + others; 25 fewer model files) |
| #180 |
Dedup structurally-identical operation-synthesized oneOf trees (/user × 3 → 1; component-internal collections preserved) |
| #181 |
Synthesize round-trip tests for smooshed oneOf variants |
| #182 |
Don't drop properties/required when oneOf is a sibling (parse-time merge) + multi-value-enum implicit discriminator + propertyName-without-mapping synthesis + anyOf+discriminator plumb |
| #183 |
Dispatch labels-requestBody (multi-array hybrid sub-dispatch + _pickPropertyArrayElementShape picker); closes 2 stubs |
| #184 |
Round-trip correctness: nullable oneOf null-cast, EmptyObject delegation, additionalProperties named-key collision (mapHash for hashCode), parser drops required entries naming nonexistent properties — 41 → 0 generated test failures |
Follow-on PRs landed since (after the 2026-04-29 wave):
| PR |
What |
| #189 |
Smoosh inline allOf variants — RepositoryRuleDetailed's 21 inline allOf oneOf variants fold into the sealed parent; structural smoosh predicate now accepts ResolvedAllOf for inline cases |
| #190 |
Strip dead Equatable from 18 parse-tree types (Schema family, Operation, OpenApi, etc.); resolver/render Equatable kept (load-bearing for _collectionCache and _ModelCollector); coverage tests added pinning the load-bearing usages |
| #191 |
Reframe spec-iteration skill from github-primary to a rotation across all gen_tests specs; add Discord with its blockers documented |
| #192 |
Parse and render integer enums (type: integer, enum: [...]); typed SchemaStringEnum / SchemaIntegerEnum family. Closes Discord's first blocker |
| #194 |
Synthesize valid example values + move newtype validation into the newtype's constructor (regex-aware string synthesizer; range/multipleOf-aware numeric synthesizer). Closes Discord's second blocker |
The earlier dispatch + naming + smoosh arc (#157–#170, #172):
| PR |
What |
| #157–#160 |
Dispatch mode → IR + sealed DispatchDecision family |
| #161–#163, #165–#167 |
Naming pass introduced + multi-tier preferences + title-derived names |
| #168–#170 |
Smoosh series — variant data class extends sealed parent directly under predicate / discriminator / shape+hybrid dispatch |
| #172 |
Refresh skill: surface verbose-log mining as primary discovery channel |
Stub count: 7 → 3 across the night (Groups A and B closed;
Group C parked). Wrapper count: 49 → 57 (the 8 increase is
new variants from #182's parse-time merge — checks_create_request
now generates typed variant classes that previously didn't exist
as a stub). Generated test suite: 41 broken → all 6146 pass.
Open issues filed during the night that should drive next iteration:
- #185 — strategy needed for web/dart2js-aware codegen
(
format=int64 is the immediate trigger). Needs a real
validation target with near-2^63 numeric values to motivate.
Discord doesn't qualify (their snowflakes are JSON strings,
not int64). Stripe is the next candidate to pull and check.
Until a target lands, stay with current int behavior. Eric's
preference if/when this ships: Int64 from package:fixnum,
no opt-in flag.
- #186 — split request/response classes when properties are
marked
readOnly: true. Correctness issue (servers can reject
requests with readOnly fields), not just ergonomics. Eric's
preference: per-class duplication (UserRequest /
UserResponse) over dual-constructor.
- #187 — remove unused Equatable
props from tree types.
Partially closed by #190: the parse-tree (18 classes) was
genuinely dead and got stripped. Render-tree and resolver-tree
Equatable turned out to be load-bearing (_collectionCache for
#180 dedup, _ModelCollector Set dedup) — kept, with explicit
coverage tests added. The skill doc framing of "the auto-
generated == is dead infrastructure" was wrong about render
and resolver.
Open gaps (pick from here)
Discovery: don't pick from this list blind — regen first
Before assuming the gaps below are the right gaps, run a -v
regen against the spec you're targeting and tally the warnings:
spec=api.github.com # or discord, petstore, spacetraders
rm -rf /tmp/${spec//./_}_v && \
dart run space_gen -v -i ~/Documents/GitHub/personal/gen_tests/${spec}.json \
-o /tmp/${spec//./_}_v 2>&1 | tee /tmp/${spec//./_}_v.log >/dev/null
grep -E "^(Ignoring|Unused|Skipping)" /tmp/${spec//./_}_v.log \
| sed -E 's/ in #.*//; s/=[^[:space:]]+ /=… /; s/dynamic/…/; s/\([A-Za-z]+\)/(…)/' \
| sort | uniq -c | sort -rn
The categories at the top of the tally are dropping spec
semantics on the floor. Pick from this or from the list below
— don't assume the visible-output gaps are always the highest-
leverage ones.
Warnings come from _warnUnused / _warnIgnored in
lib/src/parser.dart. To see what fields a parser visit handles
vs ignores, search there.
Discord — what's next
Both pre-existing blockers are closed (#192 + #194). The next
useful step is: regen Discord with -v, tally the warnings (see
"Discovery" command above), and pick from there. Whatever now
surfaces past the deeper traversal is a feature gap that wasn't
visible while the parser was throwing.
Tracking issues (the high-leverage tracks, partially open)
#185 — Strategy needed for web/dart2js-aware code generation.
Trigger: format=int64 (246 sites in github) silently produces
broken code on dart2js for specs with near-2^63 IDs. Discord
was investigated as a target and didn't qualify (snowflakes are
JSON strings, not int64). Stripe is the next candidate — pull
the spec, check whether amounts use int64 or string-encoding
before coding. Eric's preference if/when this ships: Int64
from package:fixnum, no opt-in flag.
#186 — Split request/response classes when properties are
readOnly: true. Correctness issue: servers can reject requests
that include readOnly fields. 81 sites in github, plus
Unused: readOnly=… in allOf (3 more) and non-property slots
(12 more). Eric's preference: per-class duplication
(UserRequest / UserResponse) over dual-constructor. Should
also handle writeOnly: true symmetrically. PR #175 laid the
parser-side foundation (captures SchemaObject.readOnlyProperties)
before being closed; rebuild on top of that.
#187 — Partially closed by #190. Parse-tree Equatable was
stripped (18 classes); render-tree and resolver-tree Equatable
are load-bearing (_collectionCache for #180 dedup,
_ModelCollector Set dedup for file-emission dedup) and stay,
with explicit coverage tests now pinning them.
Smaller open gaps
<Parent>Variant<i> doubling-fallback wrapper — only 1 left
in github (GistsCreateRequestPublicVariant1, an inline string-
enum). Down from 22 before #189. Closing that last one would
need smoosh extending to inline ResolvedEnum variants too —
unclear if worth the complexity for a single site.
2 _1 collision suffixes that actually render — metadata_1
(the anyOf at metadata.additionalProperties, a String/num/Bool
union) and code_scanning_variant_analysis_status_1 (an inline
enum that's a subset of the top-level enum with the same name).
The resolver's snake-name collision handling appends _1
mechanically; could pick a more descriptive parent-context
disambiguator. Two other _1 allocations (codespace_machine_1,
repository_ruleset_conditions_1 — the Group C stub) don't
actually render their own files. Low leverage.
3 enumless-anyOf stubs (Group C): timeline-issue-events
(22 variants), issue-event-for-issue (15), repository- ruleset-conditions-1. All variants share an event: string
field but no enum to dispatch on. Skill says skip —
try-each is order-dependent and inferring values from variant
titles is fragile.
Synthesized typeName collisions. <op>_response could
collide with a schema named that way in the spec. The naming
pass enumerates both; multi-tier preferences would let the
synthesized name fall back to <op>_response_2 on collision,
but no caller currently passes that fallback list. Tiny
widening of the op-response claim list.
Error-status response unions. Multi-status dispatch only
kicks in for 2xx variation. 4xx/5xx with structurally-different
bodies still falls back to untyped ApiException<Object?>.
Range-mixed multi-status fallback. When 2XX range mixes
with explicit 2xx codes, render synthesizes a RenderOneOf
with source: null that always emits the legacy stub. Closing
this needs the synthesized oneOf to participate in dispatch.
anyOf+additionalProperties with anyOf body. github's
metadata schema. PR #174 may have already covered it via the
precedence fix at the additionalProperties slot — verify with
a -v regen check before opening a PR.
Unknown formats like repo.nwo and timestamp. Low value
to handle directly; could surface as typedef hints in doc
comments. Probably skip until a real user asks.
Conventions
From the project's CLAUDE.md and saved memory:
- Bar: "as good as handwritten" generated Dart. Push back on
shortcuts that produce ugly output.
- Validation target required: pick the next PR by what real
specs need. github is the primary punching bag.
- Don't add a new strategy when a new predicate / candidate /
knob will do. The dispatch + naming + smoosh series taught
this. Adding a 6th dispatch mode or a 5th
RenderOneOf codepath
is almost never the right move; extend an existing structure
with a new variant.
- Architectural changes ship as their own PRs first, with
byte-identical regen, before the algorithm change that
motivated them. Pattern from the dispatch series (#158 → #159),
the naming series (#161 → #162 → #163 → #165 → #166), and the
smoosh series (the platform piece in each landed dispatch type
before the next). Introduce the abstraction without behavior
change, lock it in, then swap the algorithm in a focused diff.
- Snake-keyed allocator, camel derived. A snake collision IS a
camel collision (and vice versa) — one allocator handles both.
No parallel allocator. File names and class names share storage.
- Sealed subclasses live in the same library (Dart's
sealed
modifier requires it). Smooshed variants are emitted inline in
the parent's .dart file, not their own file. The
rendersToSeparateFile predicate gates per-schema file emission;
smooshed RenderObjects return false.
- Byte-identical regen as verification gate. When a refactor
claims no behavior change, prove it: regen
~/.../api.github.com.json before and after, normalize package
names + strip the post-format // ignore_for_file: lines_longer_than_80_chars lint header (which is package-name-
length sensitive), then diff -r. Zero content diffs = clean
lift.
- Self-review before pushing. Read the diff cold with two
questions: (1) is anything here doing nothing useful (dead gates,
speculative branches), (2) are there inline checks duplicated 3+
times that want a helper. Eric will ask if you don't.
- Unit tests over gen_tests: each layer (parser, resolver,
dispatch, naming, render) has its own unit test file. Add narrow
unit tests on the layer changing; only add a
gen_tests/
fixture for genuine end-to-end coverage.
- Don't use
!: bind nullable fields to a local, null-check
the local. Same for tests — use is patterns or if (x == null) fail(...) instead of postfix !.
- American English. cspell catches British spellings — use the
American form. For coined / informal words (
smooshing,
smooshable, smooshed, dispatchable, regen, unioned,
fixpoint, prefs, camelization), just add them to
cspell.config.yaml. Rephrase or rename for British spellings.
required on internal types: every constructor parameter
on Schema* / Resolved* / Render* classes is required,
including fields that default to null/[].
- Sealed types over nullable-pair encodings when a field
encodes "exactly one of N." Examples:
OperationReturn,
DispatchDecision, Predicate, _DispatchMode.
- Exhaustive switches on sealed types, not if-chains, when the
decision is mutually exclusive across known kinds. Adding a new
subtype becomes a compile-time prompt to decide. The
_dispatchEmitsSmooshed post-self-review change is the recent
example.
- 80-col wrap for code; loose for doc comments.
- Don't edit CHANGELOG.md in regular PRs — the release skill
generates it from PR bodies.
- Separate small PRs ("one gap per PR") over bundled changes
— keeps github regen diff crisp per PR.
- Every PR branches from
origin/main. Never stack a new gap
fix on top of an in-progress branch. If you accidentally stack
(forgot to branch fresh), the cleanest recovery is to combine
related work into one PR and update the title/description rather
than try to untangle the git history.
- Inline small follow-ups in the same PR — don't file separate
issues for them. Human review time is the expensive resource;
every separate PR is another full review cycle. When self-
review surfaces a small refactor or cleanup, do it now in the
same PR. Examples folded inline during the 2026-04-29 review:
markUsed API rename + four call-site updates (#182), dropping
the dead _collectionSnakeStem('allOf') branch (#182), splitting
_pickPropertyArrayElementShape into its own picker (#183),
refactoring all four _buildXMode builders to take their
dispatch object directly (#183), filing the parser-side required-
references-unknown-property warn instead of the render-side
invalidJsonExample band-aid (#184). Each was small enough to
fit; collectively they kept the PRs at proper checkpoints.
- Each PR is a measurable checkpoint. Some changes legitimately
span multiple PRs, but each one should land at a meaningful
state, not "halfway through a refactor." If review surfaces "we're
just a few little bits away from the checkpoint," pull those
bits in.
- Don't dodge symptoms with band-aids when the real fix is
upstream. PR #184 originally filtered
invalidJsonExample to
silence an asserted-throw test for github's
package-version-metadata-docker typo (required: [tags] for a
property called tag). Real fix: parser detects the unknown
name, drops it from requiredProperties, and warns. Downstream
stages then only see real names — no per-stage workarounds
needed. Apply this everywhere: a render-time filter compensating
for a parser-time miss is a smell.
- Verbose-log-mining is the primary discovery channel. The
-v regen surfaces gaps that visible-output-only review misses.
Run the tally one-shot before assuming the previously-tracked
gap list is current — it's typically out of date by 1-2
iterations of the project.
Working rhythm
Branch off origin/main (git checkout -b es/<slug> origin/main). Always — even if the worktree currently sits on
the previous PR's branch and that PR hasn't merged yet. Don't
stack PRs. Don't git checkout main in this worktree (the
primary checkout owns main).
Read the relevant layer (lib/src/parse/,
lib/src/resolver.dart, lib/src/dispatch.dart,
lib/src/naming.dart, lib/src/render/render_tree.dart,
lib/src/render/file_renderer.dart, lib/templates/*.mustache)
for context.
Add a unit test exercising the gap.
Make the fix in the right layer.
Render-layer unit tests in test/render/render_schema_test.dart,
test/render/render_operation_test.dart, or
test/naming_test.dart. Don't snapshot full template output —
contains() checks read better and survive whitespace tweaks.
End-to-end smoke test (when warranted): write a small spec at
/tmp/<name>_repro/spec.json, run dart run space_gen -i ... -o ..., verify generated code in a Dart test.
Regen against every tracked spec — github first (the bar to
maintain), then the others (which surface different patterns).
Don't ship a PR that regresses any spec already at "clean";
for specs not yet at "clean" (Discord today), each PR should at
least not make things worse:
for spec in api.github.com discord petstore spacetraders; do
out=/tmp/${spec//./_}_out
rm -rf "$out"
dart run space_gen \
-i ~/Documents/GitHub/personal/gen_tests/${spec}.json \
-o "$out" || echo "FAIL: $spec"
(cd "$out" 2>/dev/null && dart analyze 2>&1 | tail -1)
done
github should be No issues found!. Others surface their own
gaps — useful signal for picking what to burn down next.
Add -v (--verbose) to surface detailed warnings that
the default run swallows — unsupported feature usage, name
collisions resolved by renaming, schema-shape fallbacks, etc.
For "byte-identical" claims: baseline with git stash,
regen, restore, regen again, diff. The package-name-in-paths
problem only shows up if the two output dirs have different
names — keep the path identical (e.g. always /tmp/github_out)
to avoid that source of noise.
Count newly-fixed sites if the PR closes a stub-emitting gap:
grep -l "throw UnimplementedError" /tmp/github_out/lib -r | wc -l
Self-review before pushing. Read your diff cold. If
something is doing nothing (dead gate, speculative branch),
delete it. If a check is duplicated across 3+ files, extract
a helper. If a comment claims something that's no longer true
("this is a partial-rollout switch"), update it. Eric will
ask "self-review please" and you should already have done it.
Push, open PR. CI will:
- cspell trips on coined words — add them to
cspell.config.yaml. Rephrase only for British spellings.
- codecov/patch checks new-line coverage.
- codecov/project may fail with no-drop default — mostly an
accounting artifact when adding lines that shift uncovered
code's denominator. Address only if patch coverage is
genuinely low.
- dart format must pass.
Don't
- Don't snapshot-match full template output in tests — fragile.
- Don't pivot to gen_tests fixtures for unit-level coverage.
- Don't use postfix
! to silence Dart's flow analysis.
- Don't bundle multiple gap fixes in one PR — separate PRs.
- Don't merge without
dart analyze clean on the github regen.
- Don't touch the merge-conflict-prone CHANGELOG.md in feature PRs.
- Don't conflate operation-level constructs with schema-level ones.
(#148 made this lesson explicit: multi-status responses look
like a oneOf but aren't.)
- Don't add a new dispatch mode / template branch /
_DispatchMode
subtype when a new predicate kind or new picker that builds the
existing mode will do.
- Don't compute names in render. Naming is the source of truth;
render reads. If a code path needs a name, it goes through
AssignedNames (or a strict-mode _requireAssignedName()
throws telling you why).
- Don't ship infrastructure with no production user. The
NameAllocator multi-tier path was almost shipped untested in
production — the mistake (a multi-tier list that was worse
than single-tier in conflict) was caught in self-review. Today
every dispatch builder is the production user of
isSmooshed;
multi-tier is the production user via title-based renaming. Hold
the line.
- Don't add a smooshed-variant inline emission case for "future-
proofing" without a concrete trigger. Removed one such
speculative branch in #170 self-review — it would silently mis-
render if ever reached. Better to crash on missing-case than
silently produce wrong output.
- Don't ship "swap on better arrival" mutation in immutable caches.
Tried this for #180's name selection: kept the cache, swapped
the entry when a more-canonical pointer arrived. Doesn't work
— the FIRST caller already holds the loser instance, so
callers end up with two distinct instances and dedup silently
fails. Either fix at parse-order time or accept first-arrival
semantics (#180 ended up with a narrower scope rule instead).
- Don't dispatch fan-out subagents without telling them to stay
in their assigned worktree. CLAUDE.md absolute paths can mislead
subagents into editing the parent worktree. One agent during the
fan-out caught itself before pushing — only because nothing else
had uncommitted changes. Every fan-out prompt should explicitly
say: "Edit and commit ONLY in the directory you start in (
pwd
to confirm). Do NOT touch the parent worktree." See memory file
feedback_subagent_worktree_isolation.md.
Helpful one-shots
# Burn-down rotation: regen each tracked spec, report analyze
# status for each. Use this to spot regressions or pick the
# next target.
for spec in api.github.com discord petstore spacetraders; do
out=/tmp/${spec//./_}_out
rm -rf "$out"
dart run space_gen \
-i ~/Documents/GitHub/personal/gen_tests/${spec}.json \
-o "$out" 2>&1 | tail -1
echo "$spec: $((cd "$out" 2>/dev/null && dart analyze 2>&1 | tail -1) \
|| echo 'GEN FAILED')"
done
# Discovery: tally what `-v` regen says we're dropping. Run this
# first when picking the next gap — categories at the top of the
# tally are usually higher-leverage than visible-output ugliness.
# Substitute `discord_v.log` etc. for other specs.
grep -E "^(Ignoring|Unused|Skipping)" /tmp/github_v.log \
| sed -E 's/ in #.*//; s/=[^[:space:]]+ /=… /; s/dynamic/…/; s/\([A-Za-z]+\)/(…)/' \
| sort | uniq -c | sort -rn
# Drill into one category — see the actual sites
grep "Unused: oneOf" /tmp/github_v.log | head -20
# Are we still emitting stubs? (works for any spec's regen dir)
grep -l "throw UnimplementedError" /tmp/github_out/lib -r | wc -l
# Sealed dispatch sites in models/
grep -l "^sealed class " /tmp/github_out/lib/models/ -r | wc -l
# Multi-status dispatch sites in api/
grep -c "switch (response.statusCode)" /tmp/github_out/lib/api/*.dart 2>/dev/null \
| awk -F: '{ sum += } END { print sum }'
# Smoosh sites — variants that extend a sealed parent directly
grep -E "^final class \w+ extends \w+" /tmp/github_out/lib/messages/*.dart | wc -l
# Wrappers still using the post-#165 `Variant<i>` doubling fallback
grep -rE "class \w+Variant[0-9]+ extends \w+" /tmp/github_out/lib | wc -l
# Generated test suite — sanity check after a render-layer change
(cd /tmp/github_out && dart pub get && dart test 2>&1 | tail -3)
# Coverage for a specific file (after running ./coverage.sh)
awk '/^SF:.*\/parser.dart$/,/^end_of_record$/' coverage/lcov.info \
| grep "DA:.*,0$"
# Spec-side analysis (Python is fine; the spec is JSON):
python3 -c "
import json
spec = json.load(open('$HOME/Documents/GitHub/personal/gen_tests/api.github.com.json'))
def walk(n, p):
if isinstance(n, dict):
if 'oneOf' in n:
print(p, [v.get('type', 'ref') if isinstance(v, dict) else '?' for v in n['oneOf']])
for k, v in n.items(): walk(v, p + '/' + str(k))
elif isinstance(n, list):
for i, x in enumerate(n): walk(x, p + '/' + str(i))
walk(spec, '')
" | head -20
Where to look
lib/src/parser.dart, lib/src/parse/ — spec → Spec parse tree.
lib/src/resolver.dart — Spec → ResolvedSpec, $ref + snake-name
collision resolution. Stays Dart-blind.
lib/src/dispatch.dart — decideDispatch(ResolvedSchemaCollection)
per oneOf/anyOf, sealed DispatchDecision family + Predicate
IR.
lib/src/naming.dart — assignNames(ResolvedSpec) walks the
resolved tree and dispatch decisions to assign every Dart class
name. NameAllocator is the multi-tier preference allocator.
AssignedNames exposes both snakeFor(pointer) and []/
maybeGet (camel, derived). Smoosh metadata in
smooshedParentSnakeByPointer / parentSealedTypeFor.
lib/src/render/render_tree.dart — ResolvedSpec → RenderSpec.
RenderOneOf._buildXMode per dispatch kind. RenderObject. parentSealedTypeName carries the smoosh marker;
RenderSchema.isSmooshed is the predicate used by builders +
file_renderer.
lib/src/render/file_renderer.dart — rendersToSeparateFile
gates per-schema file emission (smooshed variants return false).
Per-schema and per-API import collection.
lib/templates/ — Mustache templates. schema_object.mustache
emits final class X extends Y when parentSealedTypeName is
set. schema_one_of.mustache has four dispatch sections; each
reads caseExpression per arm and includes the schema_object
partial for smooshedVariants.
test/render/render_schema_test.dart,
test/render/render_operation_test.dart,
test/naming_test.dart — unit tests per layer.
~/Documents/GitHub/personal/gen_tests/api.github.com.json —
primary validation target.
ARCHITECTURE.md — pipeline diagram, per-phase notes, planned
next steps. Update when phases shift.
- Issue #144 on github — running list of oneOf-specific
follow-ups. Other gaps surface fresh from regen runs.
Source: eseidel/space_gen — distributed by TomeVault.
1---2name: eseidel-space-gen-space-gen3description: State as of 2026-04-30 (Discord blockers cleared)4---56# State as of 2026-04-30 (Discord blockers cleared)78## What this is910space_gen is iterating toward a "fantastic" Dart OpenAPI generator.11The methodology: run real-world specs through it, see what breaks12or reads ugly, fix one thing per PR. Each PR should leave every13tracked spec at least as good as it was — ideally better.1415**Validation targets** (in `~/Documents/GitHub/personal/gen_tests/`,16parallel to the repo, not checked in):1718- **github** — `api.github.com.json`, ~1000 operations, the primary19 punching bag. Exercises everything (oneOf, discriminators,20 multi-status responses, big enums, recursion, status code ranges,21 complex error handling). Currently `dart analyze` clean on the22 regen with all 6100+ generated round-trip tests passing — the23 bar to maintain.24- **Discord** — `discord.json`, ~511 schemas / 141 paths. OpenAPI25 3.1. Snowflake IDs are `type: string` with a numeric pattern (so26 no int64 wire-format trap), but exposes patterns github doesn't:27 bitfield-style integer enums (132 sites, closed by #192), and28 `type: string` newtype schemas with regex patterns (32729 `validatePattern` call sites, closed by #194). Both blockers30 cleared as of 2026-04-30 — Discord regen progresses well past31 the parser-throw and the validation-call-site bug. Remaining32 failures are different gaps surfaced by the now-deeper traversal.33- **petstore** — `petstore.json`, the OpenAPI canonical example.34 Smoke test for the basics.35- **spacetraders** — `spacetraders.json`. Smaller real spec.36- **train-travel** — `train-travel.yaml`. YAML-format coverage.3738There's also `private_gen_tests/watchcrunch-api.json` — a user-39reported spec used as a third-party validation target (not Eric's40own service).4142A "gap" can be any of:43- `UnimplementedError` stub in generated code (always wrong).44- Generated Dart that doesn't compile, doesn't analyze clean, or45 fails its own round-trip tests.46- Output that's correct but ugly (double-prefix names, redundant47 `?`, fake oneOf wrappers, blank lines, etc.).48- Missing OpenAPI feature (cookie params, multipart edge cases,49 spec-side `nullable: true` quirks, etc.).50- A lint that fires on the generated code (`comment_references`,51 `prefer_single_quotes`, `lines_longer_than_80_chars`).52- A real-spec author hits a snag and reports it.5354**Github status:** `dart analyze` clean on the regen. **3 stubs55remain** (Group C only — the enumless anyOfs the skill suggests56skipping). Stub Groups A (validation-error twins) and B (labels57requestBody) closed in PRs #177 and #183. Generated test suite:586100+ tests, all passing — 41 round-trip-correctness bugs fixed59in #184. Smoosh series complete: #168–#170 (predicate /60discriminator / shape+hybrid dispatch), #179 (exclusive-by-use61top-level $refs), #189 (inline allOf — `RepositoryRuleDetailed`'s6221 wrappers folded into the sealed parent). Naming and dispatch63arcs settled; structural dedup of operation-synthesized oneOfs64landed in #180.6566**Discord status:** Both pre-existing parser/render blockers are67now closed:6869- **Integer enums (132 sites)** — closed by **#192**. Generalized70 `SchemaEnum.enumValues` from `List<String>` to a typed family71 (`SchemaStringEnum` / `SchemaIntegerEnum`) mirroring the existing72 `SchemaNumeric<T>` split. Composes with the multi-value-enum73 implicit discriminator picker from #182.74- **Pattern validation on newtype Strings (327 call sites)** —75 closed by **#194**, taking the principled route. Validation now76 lives in the newtype's own constructor (validate once at77 construction) rather than at every API call site. Required78 making synthesized example values actually satisfy their schema79 (regex-aware candidate testing for strings, range/multipleOf-80 aware for numerics) so the auto-generated round-trip tests81 don't throw `ArgumentError` from `const Foo('example')` against82 a regex newtype.8384The next thing to do with Discord is **regen and assess** — the85deeper traversal will surface gaps that the parser-throw was86masking. Run a `-v` regen and tally the warnings (see "Discovery"87below).8889Discord notably does NOT motivate the int64/web design call from90issue #185: their snowflake IDs are JSON strings with a numeric91pattern, not `format: int64` — they sidestepped the precision92problem by design. Only 7 int64 sites in the spec total, and one93schema is literally named `Int53Type` to acknowledge the JS limit.9495**The picture beyond stubs and visible-output ugliness.** A `-v`96regen surfaces feature gaps the skill historically didn't track.97The github 2026-04-29 baseline:9899| Count | Category | What | Status |100|------:|---|---|---|101| 246 | `Ignoring: format=int64 (String)` | int64 transmitted as JSON string for precision | tracked in **issue #185** (web-aware codegen — needs design) |102| 81 | `Unused: readOnly=true` | response-only fields not marked as such | tracked in **issue #186** (request/response split) |103| 19 | `Unused: x-multi-segment=true` | github vendor extension (path-segment hint) | known; `-v` log only — no action needed |104| 12 | `Ignoring: readOnly=…` (non-property slots) | readOnly outside a property — composes with #186 | tracked in **issue #186** |105| 8 | `Unused: readOnly=false` | spec noise — explicit `false` matches default | acceptable noise |106| 57 | `<Parent>OneOf<i>` wrappers in regen | naming gap — variants the heuristics don't catch | open (low priority — see "Naming polish") |107| 6 | `_1` collision suffixes | snake-name collision resolution | open (low priority) |108| 3 | enumless-anyOf stubs | Group C — skill says skip (try-each is order-dependent) | parked |109| 2 | `Ignoring: required (List<…>)` | parser drops `required: [foo]` when `foo` isn't a real property | landed in #184; the warn-log is the feature |110| various | unknown formats | `repo.nwo`, `timestamp` | open (low value) |111112Gen one of these and you'll likely find more than one PR's worth113of work. **Mine the verbose log first** — see "Discovery" below114under "Open gaps". The naming/smoosh/dispatch arc has settled;115the next high-leverage tracks are issue #185 (web-aware codegen)116and issue #186 (readOnly request/response split) — both need117design passes before code.118119## Architecture120121The pipeline:122123```124Load → Parse → Resolve → Dispatch → Naming → Render125 │ │126 ▼ ▼127 sidecar lookups (Decisions, Names)128```129130- **`lib/src/dispatch.dart`** (#160). Pure-structural pass: for131 each `ResolvedSchemaCollection`, picks a `DispatchDecision`132 (`Discriminator` / `Shape` / `Hybrid` / `Predicate` / `NoDispatch`).133 Variants are referenced as `ResolvedSchema`, no Dart names.134 `Predicate` IR (`KeyExists`, `ArrayElementHasKey`, `Always`) —135 adding a new predicate is a subtype, not a new mode.136137- **`lib/src/naming.dart`** (#161–#163, #165, #166, #167). The138 source of truth for every Dart class name *and* every file139 basename. Single `NameAllocator` keyed by snake names; camel is140 derived via `camelFromSnake` on read. Each entity submits a141 *preference list* (best/shortest first, longest/safest last);142 the allocator runs a fixpoint that advances colliding entities to143 their next preference, then numeric-suffix-disambiguates any144 remaining ties. Order-independent.145146 - **Two-phase resolution**: phase 1 claims schemas + synthesized147 op-response names; phase 2 claims wrapper subclass names (which148 depend on phase-1 resolved variant names). `AssignedNames`149 exposes both `snakeFor(pointer)` and `[]`/`maybeGet` (camel,150 derived).151 - **Title-derived names** (#167): inline oneOf/anyOf variants152 with a spec `title:` opt into a multi-tier preference list153 `[<title-snake>, <parent>_one_of_<i>]`. When the title is154 unique, the variant gets the descriptive name (file + class155 paired). github example: `org-ruleset-conditions` variants156 flip to `RepositoryNameAndRefName` etc.157 - **Doubling fallback** (#165): when a wrapper's158 `<parent><variantTag>` would double the parent prefix (because159 the parser-synthesized inline variant name already starts with160 the parent), the wrapper falls back to `<parent>Variant<i>`.161162- **`SpecResolver`** (in `render_tree.dart`) consults both pre-163 passes: `decideDispatch(source)` per oneOf, and a `names=` field164 populated from `assignNames(spec)` at the start of `toRenderSpec`.165 Test helpers (`renderTestSchema`, `renderTestOperation`, …)166 populate names with `assignNamesForSchema(...)` /167 `assignNamesForOperation(...)`.168169**Strict-mode invariant** (#162): `RenderSchema.typeName` and170`RenderOneOf.wrapperTypeName` throw `StateError` if the lookup is171missing. Production crashes loudly if any render path forgets to172populate names. Tests that build schemas directly without going173through `SpecResolver` must pass `assignedName: 'Foo'` explicitly.174175**Resolver stays Dart-blind.** It carries `snakeName` (parser-176level) and resolves snake-name collisions with `_1` suffixes; it177knows nothing about Dart class names.178179### Smoosh (the structural matrix)180181For an inline `ResolvedObject` variant of a oneOf/anyOf, today's182output is **smooshed** (#168, #169, #170): the variant data class183itself extends the sealed parent, inlined in the parent's `.dart`184file, with no separate wrapper subclass and no `value:` shim.185186Pattern matching destructures fields directly:187188```dart189switch (req) {190 case ProjectsCreateCardRequestOneOf0(:final note): // direct191 print(note);192}193```194195instead of unwrapping `value:`:196197```dart198case ProjectsCreateCardRequestVariant0(value: final v):199 print(v.note); // pre-smoosh200```201202Smoosh applies when:2032041. **Structural eligibility** (`_isStructurallySmooshable`): variant205 is `ResolvedObject` AND its pointer is a child of the206 collection's pointer (so it's inline-exclusive by construction).207 `ResolvedAllOf` is excluded — it collapses to a synthesized208 `RenderObject` at render but isn't a `ResolvedObject` upstream.2092. **Render support** (`_dispatchEmitsSmooshed`): the parent210 dispatch's render template emits the smooshed form. Today this211 is exhaustive — every `DispatchDecision` kind capable of smoosh212 does it (predicate-required, discriminator, shape, hybrid Map213 sub-arms). Array-element predicate is excluded structurally214 (variants are arrays, not objects); `NoDispatch` doesn't claim215 wrappers at all.216217Sealed subclasses must live in the same library — Dart's `sealed`218modifier restricts cross-library extension — so smooshed variants219are emitted **inline in the parent's `.dart` file**, not their220own. `RenderObject.parentSealedTypeName: String?` carries the221parent's class name; `RenderSchema.isSmooshed` is the check used222by the dispatch builders and `file_renderer`.223224### Render-side dispatch builders225226Each dispatch kind has a `_buildXMode(...)` helper on `RenderOneOf`227that splits variants into `dispatch` (per-arm `caseExpression`s),228`variants` (non-smooshed wrapper subclass declarations), and229`smooshedVariants` (full `toTemplateContext`s rendered inline via230the `schema_object` partial). The four builders follow the same231shape:232233- `_buildDiscriminatorMode` — `switch (json[propertyName])`.234- `_buildShapeMode` — `switch (json) { T v => ... }`.235- `_buildHybridMode` — non-Map shape arms + Map sub-dispatch with236 optional unguarded fallback.237- `_buildPredicateMode` — if-chain (required-field or array-238 element).239240Each `caseExpression` is the full Dart expression to `return` from241its switch arm — pre-composed in render code so the template just242splices it. Smooshed: `<Variant>.fromJson(<arg>)`. Non-smooshed:243`<Wrapper>(<Variant>.fromJson(<arg>))`.244245## Recent PRs246247The 2026-04-28/29 fan-out (12 PRs in one night, 11 landed):248249| PR | What |250|---|---|251| #173 | _closed — replaced by issue #185._ Initial framing of `format=int64` was github-specific (assumed `int` was correct on VM, glossed over dart2js precision loss). |252| #174 | Handle inline `oneOf`/`anyOf` at property schema slots (precedence fix: explicit `oneOf` wins over multi-type-array expansion) |253| #175 | _closed — replaced by issues #186, #187._ readOnly doc-comment marker was a half-measure; real fix needs request/response split (#186), and the Equatable plumbing it added is the start of cleanup #187. |254| #176 | Detail-log spec-author quirks (`required` on array, `maxProperties`, etc.); drop the silent vendor-extension filter (`x-*` shows in `-v` for discoverability) |255| #177 | Dispatch validation-error twins via new `PropertyArrayItemShape` predicate; closes 2 stubs |256| #178 | Surface spec `example:` values in generated doc comments (51 sites) |257| #179 | Smoosh top-level `$ref` variants used by exactly one parent (RepositoryRule family + others; 25 fewer model files) |258| #180 | Dedup structurally-identical operation-synthesized oneOf trees (`/user` × 3 → 1; component-internal collections preserved) |259| #181 | Synthesize round-trip tests for smooshed oneOf variants |260| #182 | Don't drop `properties`/`required` when `oneOf` is a sibling (parse-time merge) + multi-value-enum implicit discriminator + propertyName-without-mapping synthesis + anyOf+discriminator plumb |261| #183 | Dispatch labels-requestBody (multi-array hybrid sub-dispatch + `_pickPropertyArrayElementShape` picker); closes 2 stubs |262| #184 | Round-trip correctness: nullable oneOf null-cast, EmptyObject delegation, additionalProperties named-key collision (`mapHash` for hashCode), parser drops `required` entries naming nonexistent properties — 41 → 0 generated test failures |263264Follow-on PRs landed since (after the 2026-04-29 wave):265266| PR | What |267|---|---|268| #189 | Smoosh inline allOf variants — `RepositoryRuleDetailed`'s 21 inline `allOf` oneOf variants fold into the sealed parent; structural smoosh predicate now accepts `ResolvedAllOf` for inline cases |269| #190 | Strip dead Equatable from 18 parse-tree types (`Schema` family, `Operation`, `OpenApi`, etc.); resolver/render Equatable kept (load-bearing for `_collectionCache` and `_ModelCollector`); coverage tests added pinning the load-bearing usages |270| #191 | Reframe spec-iteration skill from github-primary to a rotation across all gen_tests specs; add Discord with its blockers documented |271| #192 | Parse and render integer enums (`type: integer, enum: [...]`); typed `SchemaStringEnum` / `SchemaIntegerEnum` family. Closes Discord's first blocker |272| #194 | Synthesize valid example values + move newtype validation into the newtype's constructor (regex-aware string synthesizer; range/multipleOf-aware numeric synthesizer). Closes Discord's second blocker |273274The earlier dispatch + naming + smoosh arc (#157–#170, #172):275276| PR | What |277|---|---|278| #157–#160 | Dispatch mode → IR + sealed `DispatchDecision` family |279| #161–#163, #165–#167 | Naming pass introduced + multi-tier preferences + title-derived names |280| #168–#170 | Smoosh series — variant data class extends sealed parent directly under predicate / discriminator / shape+hybrid dispatch |281| #172 | Refresh skill: surface verbose-log mining as primary discovery channel |282283**Stub count: 7 → 3** across the night (Groups A and B closed;284Group C parked). **Wrapper count: 49 → 57** (the 8 increase is285new variants from #182's parse-time merge — `checks_create_request`286now generates typed variant classes that previously didn't exist287as a stub). **Generated test suite: 41 broken → all 6146 pass.**288289Open issues filed during the night that should drive next iteration:290291- **#185** — strategy needed for web/dart2js-aware codegen292 (`format=int64` is the immediate trigger). Needs a real293 validation target with near-2^63 numeric values to motivate.294 **Discord doesn't qualify** (their snowflakes are JSON strings,295 not int64). Stripe is the next candidate to pull and check.296 Until a target lands, stay with current `int` behavior. Eric's297 preference if/when this ships: `Int64` from `package:fixnum`,298 no opt-in flag.299- **#186** — split request/response classes when properties are300 marked `readOnly: true`. Correctness issue (servers can reject301 requests with readOnly fields), not just ergonomics. Eric's302 preference: per-class duplication (`UserRequest` /303 `UserResponse`) over dual-constructor.304- **#187** — remove unused Equatable `props` from tree types.305 **Partially closed by #190**: the parse-tree (18 classes) was306 genuinely dead and got stripped. Render-tree and resolver-tree307 Equatable turned out to be load-bearing (`_collectionCache` for308 #180 dedup, `_ModelCollector` Set dedup) — kept, with explicit309 coverage tests added. The skill doc framing of "the auto-310 generated `==` is dead infrastructure" was wrong about render311 and resolver.312313## Open gaps (pick from here)314315### Discovery: don't pick from this list blind — regen first316317Before assuming the gaps below are the right gaps, run a `-v`318regen against the spec you're targeting and tally the warnings:319320```sh321spec=api.github.com # or discord, petstore, spacetraders322rm -rf /tmp/${spec//./_}_v && \323 dart run space_gen -v -i ~/Documents/GitHub/personal/gen_tests/${spec}.json \324 -o /tmp/${spec//./_}_v 2>&1 | tee /tmp/${spec//./_}_v.log >/dev/null325326grep -E "^(Ignoring|Unused|Skipping)" /tmp/${spec//./_}_v.log \327 | sed -E 's/ in #.*//; s/=[^[:space:]]+ /=… /; s/dynamic/…/; s/\([A-Za-z]+\)/(…)/' \328 | sort | uniq -c | sort -rn329```330331The categories at the top of the tally are dropping spec332semantics on the floor. Pick from this *or* from the list below333— don't assume the visible-output gaps are always the highest-334leverage ones.335336Warnings come from `_warnUnused` / `_warnIgnored` in337`lib/src/parser.dart`. To see what fields a parser visit *handles*338vs ignores, search there.339340### Discord — what's next341342Both pre-existing blockers are closed (#192 + #194). The next343useful step is: regen Discord with `-v`, tally the warnings (see344"Discovery" command above), and pick from there. Whatever now345surfaces past the deeper traversal is a feature gap that wasn't346visible while the parser was throwing.347348### Tracking issues (the high-leverage tracks, partially open)349350- **#185** — Strategy needed for web/dart2js-aware code generation.351 Trigger: `format=int64` (246 sites in github) silently produces352 broken code on dart2js for specs with near-2^63 IDs. Discord353 was investigated as a target and didn't qualify (snowflakes are354 JSON strings, not int64). Stripe is the next candidate — pull355 the spec, check whether amounts use int64 or string-encoding356 before coding. Eric's preference if/when this ships: `Int64`357 from `package:fixnum`, no opt-in flag.358359- **#186** — Split request/response classes when properties are360 `readOnly: true`. Correctness issue: servers can reject requests361 that include readOnly fields. 81 sites in github, plus362 `Unused: readOnly=…` in `allOf` (3 more) and non-property slots363 (12 more). Eric's preference: per-class duplication364 (`UserRequest` / `UserResponse`) over dual-constructor. Should365 also handle `writeOnly: true` symmetrically. PR #175 laid the366 parser-side foundation (captures `SchemaObject.readOnlyProperties`)367 before being closed; rebuild on top of that.368369- **#187** — _Partially closed by #190._ Parse-tree Equatable was370 stripped (18 classes); render-tree and resolver-tree Equatable371 are load-bearing (`_collectionCache` for #180 dedup,372 `_ModelCollector` Set dedup for file-emission dedup) and stay,373 with explicit coverage tests now pinning them.374375### Smaller open gaps376377- **`<Parent>Variant<i>` doubling-fallback wrapper** — only 1 left378 in github (`GistsCreateRequestPublicVariant1`, an inline string-379 enum). Down from 22 before #189. Closing that last one would380 need smoosh extending to inline `ResolvedEnum` variants too —381 unclear if worth the complexity for a single site.382383- **2 `_1` collision suffixes that actually render** — `metadata_1`384 (the anyOf at `metadata.additionalProperties`, a String/num/Bool385 union) and `code_scanning_variant_analysis_status_1` (an inline386 enum that's a subset of the top-level enum with the same name).387 The resolver's snake-name collision handling appends `_1`388 mechanically; could pick a more descriptive parent-context389 disambiguator. Two other `_1` allocations (`codespace_machine_1`,390 `repository_ruleset_conditions_1` — the Group C stub) don't391 actually render their own files. Low leverage.392393- **3 enumless-anyOf stubs (Group C)**: `timeline-issue-events`394 (22 variants), `issue-event-for-issue` (15), `repository-395 ruleset-conditions-1`. All variants share an `event: string`396 field but no `enum` to dispatch on. **Skill says skip** —397 try-each is order-dependent and inferring values from variant398 titles is fragile.399400- **Synthesized typeName collisions.** `<op>_response` could401 collide with a schema named that way in the spec. The naming402 pass enumerates both; multi-tier preferences would let the403 synthesized name fall back to `<op>_response_2` on collision,404 but no caller currently passes that fallback list. Tiny405 widening of the op-response claim list.406407- **Error-status response unions.** Multi-status dispatch only408 kicks in for 2xx variation. 4xx/5xx with structurally-different409 bodies still falls back to untyped `ApiException<Object?>`.410411- **Range-mixed multi-status fallback.** When 2XX range mixes412 with explicit 2xx codes, render synthesizes a `RenderOneOf`413 with `source: null` that always emits the legacy stub. Closing414 this needs the synthesized oneOf to participate in dispatch.415416- **anyOf+`additionalProperties` with `anyOf` body**. github's417 `metadata` schema. PR #174 may have already covered it via the418 precedence fix at the additionalProperties slot — verify with419 a `-v` regen check before opening a PR.420421- **Unknown formats** like `repo.nwo` and `timestamp`. Low value422 to handle directly; could surface as typedef hints in doc423 comments. Probably skip until a real user asks.424425## Conventions426427From the project's CLAUDE.md and saved memory:428429- **Bar**: "as good as handwritten" generated Dart. Push back on430 shortcuts that produce ugly output.431- **Validation target required**: pick the next PR by what real432 specs need. github is the primary punching bag.433- **Don't add a new strategy when a new predicate / candidate /434 knob will do.** The dispatch + naming + smoosh series taught435 this. Adding a 6th dispatch mode or a 5th `RenderOneOf` codepath436 is almost never the right move; extend an existing structure437 with a new variant.438- **Architectural changes ship as their own PRs first**, with439 byte-identical regen, *before* the algorithm change that440 motivated them. Pattern from the dispatch series (#158 → #159),441 the naming series (#161 → #162 → #163 → #165 → #166), and the442 smoosh series (the platform piece in each landed dispatch type443 before the next). Introduce the abstraction without behavior444 change, lock it in, then swap the algorithm in a focused diff.445- **Snake-keyed allocator, camel derived.** A snake collision IS a446 camel collision (and vice versa) — one allocator handles both.447 No parallel allocator. File names and class names share storage.448- **Sealed subclasses live in the same library** (Dart's `sealed`449 modifier requires it). Smooshed variants are emitted inline in450 the parent's `.dart` file, not their own file. The451 `rendersToSeparateFile` predicate gates per-schema file emission;452 smooshed `RenderObject`s return false.453- **Byte-identical regen as verification gate.** When a refactor454 claims no behavior change, prove it: regen455 `~/.../api.github.com.json` before and after, normalize package456 names + strip the post-format `// ignore_for_file:457 lines_longer_than_80_chars` lint header (which is package-name-458 length sensitive), then `diff -r`. Zero content diffs = clean459 lift.460- **Self-review before pushing**. Read the diff cold with two461 questions: (1) is anything here doing nothing useful (dead gates,462 speculative branches), (2) are there inline checks duplicated 3+463 times that want a helper. Eric will ask if you don't.464- **Unit tests over gen_tests**: each layer (parser, resolver,465 dispatch, naming, render) has its own unit test file. Add narrow466 unit tests on the layer changing; only add a `gen_tests/`467 fixture for genuine end-to-end coverage.468- **Don't use `!`**: bind nullable fields to a local, null-check469 the local. Same for tests — use `is` patterns or `if (x ==470 null) fail(...)` instead of postfix `!`.471- **American English**. cspell catches British spellings — use the472 American form. For coined / informal words (`smooshing`,473 `smooshable`, `smooshed`, `dispatchable`, `regen`, `unioned`,474 `fixpoint`, `prefs`, `camelization`), just add them to475 `cspell.config.yaml`. Rephrase or rename for British spellings.476- **`required` on internal types**: every constructor parameter477 on `Schema*` / `Resolved*` / `Render*` classes is `required`,478 including fields that default to null/[].479- **Sealed types over nullable-pair encodings** when a field480 encodes "exactly one of N." Examples: `OperationReturn`,481 `DispatchDecision`, `Predicate`, `_DispatchMode`.482- **Exhaustive switches on sealed types**, not if-chains, when the483 decision is mutually exclusive across known kinds. Adding a new484 subtype becomes a compile-time prompt to decide. The485 `_dispatchEmitsSmooshed` post-self-review change is the recent486 example.487- **80-col wrap** for code; loose for doc comments.488- **Don't edit CHANGELOG.md** in regular PRs — the release skill489 generates it from PR bodies.490- **Separate small PRs** ("one gap per PR") over bundled changes491 — keeps github regen diff crisp per PR.492- **Every PR branches from `origin/main`.** Never stack a new gap493 fix on top of an in-progress branch. If you accidentally stack494 (forgot to branch fresh), the cleanest recovery is to combine495 related work into one PR and update the title/description rather496 than try to untangle the git history.497- **Inline small follow-ups in the same PR — don't file separate498 issues for them.** Human review time is the expensive resource;499 every separate PR is another full review cycle. When self-500 review surfaces a small refactor or cleanup, do it now in the501 same PR. Examples folded inline during the 2026-04-29 review:502 `markUsed` API rename + four call-site updates (#182), dropping503 the dead `_collectionSnakeStem('allOf')` branch (#182), splitting504 `_pickPropertyArrayElementShape` into its own picker (#183),505 refactoring all four `_buildXMode` builders to take their506 dispatch object directly (#183), filing the parser-side `required`-507 references-unknown-property warn instead of the render-side508 `invalidJsonExample` band-aid (#184). Each was small enough to509 fit; collectively they kept the PRs at proper checkpoints.510- **Each PR is a measurable checkpoint.** Some changes legitimately511 span multiple PRs, but each one should land at a meaningful512 state, not "halfway through a refactor." If review surfaces "we're513 just a few little bits away from the checkpoint," pull those514 bits in.515- **Don't dodge symptoms with band-aids when the real fix is516 upstream.** PR #184 originally filtered `invalidJsonExample` to517 silence an asserted-throw test for github's518 `package-version-metadata-docker` typo (`required: [tags]` for a519 property called `tag`). Real fix: parser detects the unknown520 name, drops it from `requiredProperties`, and warns. Downstream521 stages then only see real names — no per-stage workarounds522 needed. Apply this everywhere: a render-time filter compensating523 for a parser-time miss is a smell.524- **Verbose-log-mining is the primary discovery channel.** The525 `-v` regen surfaces gaps that visible-output-only review misses.526 Run the tally one-shot before assuming the previously-tracked527 gap list is current — it's typically out of date by 1-2528 iterations of the project.529530## Working rhythm5315321. Branch off `origin/main` (`git checkout -b es/<slug>533 origin/main`). Always — even if the worktree currently sits on534 the previous PR's branch and that PR hasn't merged yet. Don't535 stack PRs. Don't `git checkout main` in this worktree (the536 primary checkout owns `main`).5372. Read the relevant layer (`lib/src/parse/`,538 `lib/src/resolver.dart`, `lib/src/dispatch.dart`,539 `lib/src/naming.dart`, `lib/src/render/render_tree.dart`,540 `lib/src/render/file_renderer.dart`, `lib/templates/*.mustache`)541 for context.5423. Add a unit test exercising the gap.5434. Make the fix in the right layer.5445. Render-layer unit tests in `test/render/render_schema_test.dart`,545 `test/render/render_operation_test.dart`, or546 `test/naming_test.dart`. Don't snapshot full template output —547 `contains()` checks read better and survive whitespace tweaks.5486. End-to-end smoke test (when warranted): write a small spec at549 `/tmp/<name>_repro/spec.json`, run `dart run space_gen -i ...550 -o ...`, verify generated code in a Dart test.5517. Regen against every tracked spec — github first (the bar to552 maintain), then the others (which surface different patterns).553 Don't ship a PR that regresses any spec already at "clean";554 for specs not yet at "clean" (Discord today), each PR should at555 least not make things worse:556 ```sh557 for spec in api.github.com discord petstore spacetraders; do558 out=/tmp/${spec//./_}_out559 rm -rf "$out"560 dart run space_gen \561 -i ~/Documents/GitHub/personal/gen_tests/${spec}.json \562 -o "$out" || echo "FAIL: $spec"563 (cd "$out" 2>/dev/null && dart analyze 2>&1 | tail -1)564 done565 ```566 github should be `No issues found!`. Others surface their own567 gaps — useful signal for picking what to burn down next.568569 **Add `-v` (`--verbose`) to surface detailed warnings** that570 the default run swallows — unsupported feature usage, name571 collisions resolved by renaming, schema-shape fallbacks, etc.5725738. **For "byte-identical" claims:** baseline with `git stash`,574 regen, restore, regen again, diff. The package-name-in-paths575 problem only shows up if the two output dirs have different576 names — keep the path identical (e.g. always `/tmp/github_out`)577 to avoid that source of noise.5785799. Count newly-fixed sites if the PR closes a stub-emitting gap:580 ```sh581 grep -l "throw UnimplementedError" /tmp/github_out/lib -r | wc -l582 ```58358410. **Self-review before pushing.** Read your diff cold. If585 something is doing nothing (dead gate, speculative branch),586 delete it. If a check is duplicated across 3+ files, extract587 a helper. If a comment claims something that's no longer true588 (`"this is a partial-rollout switch"`), update it. Eric will589 ask "self-review please" and you should already have done it.59059111. Push, open PR. CI will:592 - cspell trips on coined words — add them to593 `cspell.config.yaml`. Rephrase only for British spellings.594 - codecov/patch checks new-line coverage.595 - codecov/project may fail with no-drop default — mostly an596 accounting artifact when adding lines that shift uncovered597 code's denominator. Address only if patch coverage is598 genuinely low.599 - dart format must pass.600601## Don't602603- Don't snapshot-match full template output in tests — fragile.604- Don't pivot to gen_tests fixtures for unit-level coverage.605- Don't use postfix `!` to silence Dart's flow analysis.606- Don't bundle multiple gap fixes in one PR — separate PRs.607- Don't merge without `dart analyze` clean on the github regen.608- Don't touch the merge-conflict-prone CHANGELOG.md in feature PRs.609- Don't conflate operation-level constructs with schema-level ones.610 (#148 made this lesson explicit: multi-status responses look611 like a oneOf but aren't.)612- Don't add a new dispatch mode / template branch / `_DispatchMode`613 subtype when a new predicate kind or new picker that builds the614 existing mode will do.615- Don't compute names in render. Naming is the source of truth;616 render reads. If a code path needs a name, it goes through617 `AssignedNames` (or a strict-mode `_requireAssignedName()`618 throws telling you why).619- Don't ship infrastructure with no production user. The620 NameAllocator multi-tier path was almost shipped untested in621 production — the mistake (a multi-tier list that was *worse*622 than single-tier in conflict) was caught in self-review. Today623 every dispatch builder is the production user of `isSmooshed`;624 multi-tier is the production user via title-based renaming. Hold625 the line.626- Don't add a smooshed-variant inline emission case for "future-627 proofing" without a concrete trigger. Removed one such628 speculative branch in #170 self-review — it would silently mis-629 render if ever reached. Better to crash on missing-case than630 silently produce wrong output.631- Don't ship "swap on better arrival" mutation in immutable caches.632 Tried this for #180's name selection: kept the cache, swapped633 the entry when a more-canonical pointer arrived. Doesn't work634 — the FIRST caller already holds the loser instance, so635 callers end up with two distinct instances and dedup silently636 fails. Either fix at parse-order time or accept first-arrival637 semantics (#180 ended up with a narrower scope rule instead).638- Don't dispatch fan-out subagents without telling them to stay639 in their assigned worktree. CLAUDE.md absolute paths can mislead640 subagents into editing the parent worktree. One agent during the641 fan-out caught itself before pushing — only because nothing else642 had uncommitted changes. Every fan-out prompt should explicitly643 say: "Edit and commit ONLY in the directory you start in (`pwd`644 to confirm). Do NOT touch the parent worktree." See memory file645 `feedback_subagent_worktree_isolation.md`.646647## Helpful one-shots648649```sh650# Burn-down rotation: regen each tracked spec, report analyze651# status for each. Use this to spot regressions or pick the652# next target.653for spec in api.github.com discord petstore spacetraders; do654 out=/tmp/${spec//./_}_out655 rm -rf "$out"656 dart run space_gen \657 -i ~/Documents/GitHub/personal/gen_tests/${spec}.json \658 -o "$out" 2>&1 | tail -1659 echo "$spec: $((cd "$out" 2>/dev/null && dart analyze 2>&1 | tail -1) \660 || echo 'GEN FAILED')"661done662663# Discovery: tally what `-v` regen says we're dropping. Run this664# first when picking the next gap — categories at the top of the665# tally are usually higher-leverage than visible-output ugliness.666# Substitute `discord_v.log` etc. for other specs.667grep -E "^(Ignoring|Unused|Skipping)" /tmp/github_v.log \668 | sed -E 's/ in #.*//; s/=[^[:space:]]+ /=… /; s/dynamic/…/; s/\([A-Za-z]+\)/(…)/' \669 | sort | uniq -c | sort -rn670671# Drill into one category — see the actual sites672grep "Unused: oneOf" /tmp/github_v.log | head -20673674# Are we still emitting stubs? (works for any spec's regen dir)675grep -l "throw UnimplementedError" /tmp/github_out/lib -r | wc -l676677# Sealed dispatch sites in models/678grep -l "^sealed class " /tmp/github_out/lib/models/ -r | wc -l679680# Multi-status dispatch sites in api/681grep -c "switch (response.statusCode)" /tmp/github_out/lib/api/*.dart 2>/dev/null \682 | awk -F: '{ sum += } END { print sum }'683684# Smoosh sites — variants that extend a sealed parent directly685grep -E "^final class \w+ extends \w+" /tmp/github_out/lib/messages/*.dart | wc -l686687# Wrappers still using the post-#165 `Variant<i>` doubling fallback688grep -rE "class \w+Variant[0-9]+ extends \w+" /tmp/github_out/lib | wc -l689690# Generated test suite — sanity check after a render-layer change691(cd /tmp/github_out && dart pub get && dart test 2>&1 | tail -3)692693# Coverage for a specific file (after running ./coverage.sh)694awk '/^SF:.*\/parser.dart$/,/^end_of_record$/' coverage/lcov.info \695 | grep "DA:.*,0$"696```697698```sh699# Spec-side analysis (Python is fine; the spec is JSON):700python3 -c "701import json702spec = json.load(open('$HOME/Documents/GitHub/personal/gen_tests/api.github.com.json'))703def walk(n, p):704 if isinstance(n, dict):705 if 'oneOf' in n:706 print(p, [v.get('type', 'ref') if isinstance(v, dict) else '?' for v in n['oneOf']])707 for k, v in n.items(): walk(v, p + '/' + str(k))708 elif isinstance(n, list):709 for i, x in enumerate(n): walk(x, p + '/' + str(i))710walk(spec, '')711" | head -20712```713714## Where to look715716- `lib/src/parser.dart`, `lib/src/parse/` — spec → Spec parse tree.717- `lib/src/resolver.dart` — Spec → ResolvedSpec, $ref + snake-name718 collision resolution. Stays Dart-blind.719- `lib/src/dispatch.dart` — `decideDispatch(ResolvedSchemaCollection)`720 per oneOf/anyOf, sealed `DispatchDecision` family + `Predicate`721 IR.722- `lib/src/naming.dart` — `assignNames(ResolvedSpec)` walks the723 resolved tree and dispatch decisions to assign every Dart class724 name. `NameAllocator` is the multi-tier preference allocator.725 `AssignedNames` exposes both `snakeFor(pointer)` and `[]`/726 `maybeGet` (camel, derived). Smoosh metadata in727 `smooshedParentSnakeByPointer` / `parentSealedTypeFor`.728- `lib/src/render/render_tree.dart` — ResolvedSpec → RenderSpec.729 `RenderOneOf._buildXMode` per dispatch kind. `RenderObject.730 parentSealedTypeName` carries the smoosh marker;731 `RenderSchema.isSmooshed` is the predicate used by builders +732 `file_renderer`.733- `lib/src/render/file_renderer.dart` — `rendersToSeparateFile`734 gates per-schema file emission (smooshed variants return false).735 Per-schema and per-API import collection.736- `lib/templates/` — Mustache templates. `schema_object.mustache`737 emits `final class X extends Y` when `parentSealedTypeName` is738 set. `schema_one_of.mustache` has four dispatch sections; each739 reads `caseExpression` per arm and includes the `schema_object`740 partial for `smooshedVariants`.741- `test/render/render_schema_test.dart`,742 `test/render/render_operation_test.dart`,743 `test/naming_test.dart` — unit tests per layer.744- `~/Documents/GitHub/personal/gen_tests/api.github.com.json` —745 primary validation target.746- `ARCHITECTURE.md` — pipeline diagram, per-phase notes, planned747 next steps. Update when phases shift.748- Issue #144 on github — running list of oneOf-specific749 follow-ups. Other gaps surface fresh from regen runs.750751---752> Source: [eseidel/space_gen](https://github.com/eseidel/space_gen) — distributed by [TomeVault](https://tomevault.io).753<!-- tomevault:4.0:skill_md:2026-06-29 -->