Skip to content

feat(generator): an optional special number keeps its named literals through the tri-state wrapper - #459

Merged
Blind-Striker merged 2 commits into
masterfrom
gen/refresh-232-special-number
Oct 17, 2026
Merged

Blind-Striker merged 2 commits into
masterfrom
gen/refresh-232-special-number

Conversation

@Blind-Striker

@Blind-Striker Blind-Striker commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

The generator now emits a working tri-state member for an optional-and-nullable request property whose number admits NaN and the infinities. Until now, OptionalConverterNamePolicy threw "No optional converter name exists for plan 'SpecialNumberTypeReferencePlan'". Generated output at the 2.0.29 pin is byte-identical: no pinned member has this shape. The 2.0.32 pin needs it for VoiceWebRTCTransportInput.dcid (Schema.optional(Schema.Number) in packages/protocol/src/groups/voice.ts:8).

Part of #462. This is REFRESH-232 PR A, the generator half of the 2.0.29 → 2.0.32 pin refresh. PR B, the refresh itself, follows on a base that contains this one.

What changes

  • OptionalConverterNamePolicy: a top-level special-number instantiation is named OptionalOfSpecialNumberJsonConverter. A special number inside a list or dictionary still throws the existing message, because a container's converter would go through the plain double metadata and lose the named literals.
  • OptionalConverterEmitter:
    • The de-duplication key becomes the pair (formatted type, converter name). Both representations format as double?, so a format-only key would merge them into one converter. With the pair key, two different types that share a converter name still reach SourceEmitter's duplicate-path refusal.
    • The special converter reads a JSON string that is ordinal-equal to NaN, Infinity or -Infinity as that value, and refuses any other string with a JsonException. It writes those three values back as the same strings. Null and number handling are as before.
  • ModelEmitter: a wrapped member no longer carries [JsonNumberHandling]. On an Optional<double?> member, System.Text.Json throws at metadata time: "'JsonNumberHandlingAttribute' is only valid on a number or a collection of numbers". So the converter carries the named literals instead. Required and non-wrapped optional special numbers keep the attribute unchanged.
  • Canon: one clause in docs/architecture/protocol-and-generation.md, in the tri-state converter bullet.
  • CHANGELOG: no entry, since nothing consumer-visible changes at the pin. PR B's 2.0.32 entry covers the member that uses this.

The converter matches what System.Text.Json's AllowNamedFloatingPointLiterals does on a required member. Measured on net10.0 with a source-generated context: it writes NaN as "NaN", and it refuses "nan", "1.5" and "+Infinity".

Tests

  • Red first:

    • inventory: a wrapped plain double? and a wrapped special number give exactly two converters, in order;
    • container refusal: a wrapped list or dictionary of special numbers is still refused, with the full message;
    • duplicate-path refusal: two types that share a converter name still reach the refusal (in SourceEmitterTests);
    • attribute placement: required and plain optional special numbers keep [JsonNumberHandling], and the wrapped one doesn't (in ModelEmitterTests);
    • compiled matrix round trip: a patchSpecialNumber member in the materialization matrix's request schema covers absent, explicit null, 4.5, and each named literal (exact JSON text). "nan", "+Infinity" and "1.5" each throw JsonException.
  • Mutation: seven compiling mutants, all killed, each production file restored and compared byte-for-byte:

    Mutant Killed by
    format-only de-duplication key inventory row
    the read falls through to GetDouble matrix
    the write falls through to WriteNumberValue matrix (ArgumentException on NaN)
    [JsonNumberHandling] kept on the wrapper matrix (InvalidOperationException) and the attribute row
    case-insensitive literal match matrix ("nan")
    special numbers admitted inside containers container rows
    converter-name-only key duplicate-path row
  • Existing-test edits: only additions. In the materialization matrix:

    • the request schema gains patchSpecialNumber;
    • the request-member assertions gain its wrapper, type and nullability;
    • the cleared payload and its null loop gain the member.

    Every existing assertion is unchanged.

Budget (against the merge-base)

  • Hand-written tools: 48 added lines and 3 removed, against a cap of 70.
  • Tests: 107 added lines, against a cap of 140 (one line removed).

Local check and review

  • Local check at this head on dev-01, on a quiet host, after a root Release build: slopwatch 0 issues; the Release build with 0 warnings; both format passes; verify-clones passing; --help; generate --verify current; the whole Tools.Tests project at 2086/0, Packaging included. CI is the full gate.
    • Bun 1.4.3, off the 1.4.2 pin.
  • Harness verify: the guards, the five touched classes, the whole Tools.Tests project (2072/0), verify-clones, generate --verify and the whitespace pass.
  • Review: the coordinator's Claude review and a fresh cross-family review. Neither found a behaviour defect. Both flagged test placement, and this PR fixes it: the source-emitter and model-emitter rows moved to their subjects' classes.
  • Coordinator nits, fixed here:
    • the converter's JsonException now carries a message;
    • the canon names the three literals;
    • the literal table has a summary.

Provenance: Generator lane / opencode dispatch harness (openai/gpt-6.1-sol-1m), coordinated, reviewed and finished in Claude Code.

@Blind-Striker
Blind-Striker added this pull request to the merge queue Oct 17, 2026
@Blind-Striker Blind-Striker mentioned this pull request Oct 17, 2026
2 of 4 tasks
Merged via the queue into master with commit 7899ecd Oct 17, 2026
12 checks passed
@Blind-Striker
Blind-Striker deleted the gen/refresh-232-special-number branch October 17, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant