Skip to content

🐛 fix: a number and a date print in the page's culture - #637

Merged
edgarmesquita merged 147 commits into
mainfrom
fix/a-number-and-a-date-print-in-the-pages-culture
Oct 11, 2026
Merged

edgarmesquita merged 147 commits into
mainfrom
fix/a-number-and-a-date-print-in-the-pages-culture

Conversation

@edgarmesquita

@edgarmesquita edgarmesquita commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Closes #454
Closes #455
Closes #469
Closes #470
Closes #471
Closes #634
Closes #570

What

A number and a date print in the browser as .NET prints them on the server, in the culture the page was rendered in, whatever path C# writes them through.

EQ2109 is retired. The public surface of eQuantic.UI.Compiler replaces Eq.AsInteger and Eq.AsSingle with Eq.AsNumber and gains TypeSymbolExtensions.IsDate, and eQuantic.UI.Web gains CultureFormatBridge. The developer surface does not move.

Why

The server renders a page in the request's culture, and the browser wrote numbers and dates its own way, so the SSR markup and the hydrated page printed one value two ways: 1.5 where the server wrote 1,5 in pt-BR, a currency with a no-break space where the server wrote a plain one, a DateOnly in a pattern nobody asked for. On an app with no .resx, the browser formatted in its host's locale, and the conformance suite passed or failed by the machine it ran on.

OpenSpec

a-number-and-a-date-print-in-the-pages-culture, archived in this pull request. It creates page-culture (the format culture a page is in, how it travels, the invariant culture with none installed, a standard number format laid out from the data, a calendar's names) and extends transpiler-bcl (a number's text with no specifier, a number's kind) and runtime-dates (the date types and a custom picture). Its design says what is not here: the culture a server action runs in (#580), a DateTimeOffset's local time (#626, which #660 has landed since), and a non-Gregorian default calendar.

Proof

A/B against main (98c2b18, with #660, #748 and the whole queue landed), the same 222 conformance cases on both sides, both executed, with main's side given the culture as main installs it on a page with a catalog, its best case. 176 fail on main and all 222 pass here: 49 of the 70 number texts in five cultures, 7 of the 11 number kinds, all 6 standard layouts, 74 of the 75 date type texts, 38 of the 55 date pictures and 2 of the 5 harness cases. The other 46 already held on main and stay as pins. Against main at dc4a410 it was 187 of 222: #660 has taught a date's picture its offset and its kind since, ten cases, and #748 a builder's interpolated holes their text, one. Main's side was built on #708's head 78215e9, whose tree is main's (8833abd1 on both). The default text's seven cases, run through the module graph an app's build writes in TypeScript and in JavaScript, fail in both on the head before them (446542b) and pass here.

The format subset is generated from .NET and replayed through the runtime byte for byte, in seven cultures, ar-EG and fr-FR among them. It folded the no-break spaces on both sides, which is how #634 passed it.

The suites ran on a7155cc, a tree the merge of main 98c2b18 left as it was, since that merge records #708's squash with the ours strategy: Compiler 1898, Web 1021 against this branch's wiki pages, Server 374, Conformance 5087 with 1 skipped, and the runtime's 2355 in 170 files (tsc and vitest through the embedded bun). The only commit after it changes the ledger line's numbers, and the Web suite, the one that reads the ledger, ran again on it: 1021. The served runtime is 223,575 bytes gzipped against the 219,741 main serves (3,834 more, +1.74%, main's bundle built and measured the same way, and its record says the same), and its budget is regenerated to what this branch serves: the layout of N, F, C and P and the date types' formatter, the twins, whose numbers now say their kind, and the runtime types that now write their full name.

Main moved under this branch more than once, and its pull requests changed the same files for their own reasons. Each merge keeps both behaviours.

The merge with #667 met two things. Its builder refuses a null string handed to a ranged overload, as .NET does, and this branch wrote every bound value through the culture's text, which made that null an empty string. A value that is text already (a string, a char, a char[], a builder) now passes as it is, and only a number, a bool, an enum or an object is written in the culture. It also pinned an exception's interpolated message as it was before the culture, and that message now prints the number's hole in the culture. And the Windows runner failed one case on the previous head, where AppendLine writes the host's "\r\n": the case reads "\r\n" as "\n" on both sides.

And it runs, on this head: the dashboard sample, built with no warning and served from its built binary, wrote R$ 1.234,50 · 98,2% · 13/08/2026 on its localization screen in pt-BR, byte for byte what the server rendered, with a plain space after R$ on both sides, and wrote it again when the browser rendered the screen after a click. It switched to es with no reload by fetching /_equantic/culture/es.json and wrote exactly the server's es text, 1.234,50 ¤ · 98,2 % · 13/8/2026 (es is a neutral culture, with the generic ¤), and its charts' axes after a client navigation read as the server rendered them (50.000, 100.000), with no console error.

Checklist

  • dotnet test on the affected test projects, and dotnet build src/eQuantic.UI.Runtime -t:TestRuntime if TypeScript changed (the embedded Bun runs tsc, then vitest run)
  • This PR's checks include a run named CI with jobs in it (build-packages, test (ubuntu-latest), test (windows-latest), openspec, session-start, wiki-checkout). A workflow whose expression does not parse creates zero jobs, and the ruleset's required checks then keep the PR blocked. GitHub lists the workflow by its file path instead of CI. While GitHub Actions has no credits, the same run is on eQuantic Space (eqs runs ls)
  • ./scripts/check-openspec.sh passes: the OpenSpec change validates strictly, and it is archived before the merge
  • dotnet build samples/DefaultUIDashboard (and PhotonDesktop / WalletMobile if the native track changed): CI's samples job builds all three on macOS only (🐛 fix: the SDK does not dictate which Xcode you have, and the pipe now builds the apps #149), the other hosts are yours
  • A broken contract has a line in the migration notes (we are in preview: break freely, hide nothing)
  • The documentation changed with the behaviour: this repository's Markdown here, and the wiki page in English AND Portuguese, in one commit on a branch of the wiki repository named exactly like this pull request's branch, merged into the wiki's master when this merges (the twin lives at locale/pt-BR/<Page>-pt-BR.md there)
  • One docs/LEDGER.md line for this event, citing the issue
  • The diff reviewed by its author before the PR opened (in Claude Code, /code-review high), every Copilot thread answered and resolved, and a new round asked for (gh pr edit <n> --add-reviewer @copilot) only after a defect, and three rounds at most
  • No "widget" in prose: the project's word is component

The OpenSpec change for #454, #455, #469, #470 and #471: the format culture
travels with every page from .NET's own data, every path C# writes a number
or a date through writes it in that culture, a number's kind travels to the
formatter, and the date types print through the formatter a DateTime does.
The server renders a page in the request's culture, and the browser wrote
numbers and dates its own way: a number with no specifier in its invariant
text (#454), a whole double through D, X and B and a native integer as a
double (#455), DateOnly, TimeOnly and DateTimeOffset through their twins'
own toString (#469), a picture's separators, offset and era as letters
(#470), and with no string catalog no culture at all, its host's locale in
its place (#471).

- The format culture travels with every page: CultureFormatBridge writes
  what the browser's formatter reads from .NET's NumberFormatInfo and
  DateTimeFormatInfo into __EQ_CULTURE__.format, boot installs it before
  hydration, and a culture switch fetches /_equantic/culture/{name}.json. A
  page with no culture installed is in the invariant one. The string
  catalogs carry strings only.
- A number or a date written into text goes through the formatter in every
  shape C# writes it, and the compiler tells the formatter a number's kind.
- The date types print through the formatter, and a custom picture writes
  the culture's separators, the offset and the era.
- The Mermaid layout writes its paths and view boxes in the invariant
  culture, as text a parser reads. EQ2109 is retired.

The served runtime grows from 190,570 bytes gzipped on main to 192,839
(+2,269, +1.19%), the budget regenerated with EQ_UPDATE_RUNTIME_BUDGET=1:
the formatter's culture data and its date types, the culture install and
switch, and the formatter calls of the transpiled components' text.
…m the culture's data

The review of this branch found a regression and the paths the culture did not reach yet.

- An int JavaScript holds as -0 (`-1 / 2` truncates to it) printed `-0`, where main and .NET print
  `0`: the text with no specifier passed no kind for an integer. Every number but a double says what
  it is there too now, `string.Format` boxes it the same way, and an integer writes its digits with no
  sign on a zero.
- `StringBuilder.Append`, `AppendLine` and `Insert`, `Convert.ToString` and a record's text wrote a
  number as JavaScript writes it. They write it as a concatenation does, in the culture in force. A
  record's text goes through the runtime's record text, which a data twin already used, each member
  with its kind or a function of its value where the type says more than the value does.
- `N`, `F`, `C` and `P` were laid out by `Intl` (#634): ar-EG's own digits where .NET writes ASCII
  ones, a no-break space where .NET's currency and percent patterns have a plain one, and two digits
  for `N` where .NET reads three on ICU. They are drawn from the culture's `NumberFormatInfo` now,
  whose patterns, symbols, separators, group sizes and digits travel with the page, and the ISO
  currency code nothing reads any more is gone. The format subset compares byte for byte, in seven
  cultures, where it folded the no-break spaces on both sides.
- A calendar's names travelled twice on every page, as a catalog beside the format data. They are
  the format data's now, one copy, and with no culture installed a calendar is the invariant
  culture's, as a date is.

EQ2110's row, the change's proposal, design, specs and tasks follow. The served runtime's budget is
194,065 bytes gzipped, 1,226 over this branch's previous head: 559 for the runtime's code (the layout
of N, F, C and P, an integer's text, the calendar's reading) and 667 for the twins, whose numbers now
say their kind. A record's text through the runtime's helper costs less than the template it
replaced did.
…d and in the ledger

The change's proposal, design, specs and tasks carry what its review added: every path a number takes
into text, N, F, C and P from the culture's data (#634), and a calendar's names from that data, one
copy. Archived with `openspec archive`, which creates `page-culture` and extends `runtime-dates` and
`transpiler-bcl`, and one ledger line with the A/B against main: 182 of the 217 conformance cases
failed there, with the culture installed as main installs it on a page with a catalog.
Main brought #621, a class built as C# builds it. Every conflict was a generated file: the runtime's
twins, regenerated with the merged compiler, whose record text goes through the runtime's helper over
#621's stores, and the served runtime's budget, measured on the merge at 195,582 bytes gzipped
(main serves 192,040).
…ught

#621's two lines merged first, and a ledger is oldest first.
…wner

A nested class had no module, resolved to a top-level class of its name, and a nested record or struct wrote a module a top-level one of its name writes too (#584). Each nested class, record and struct gets a twin named by its owner and itself, joined by $ (Cart$Item), which every reference names.
A class declared inside another one had no module, so constructing it named nothing, and where a
top-level class had its name it resolved to that one (#584); a nested record wrote the module a
top-level record of its name writes, which the build refused (EQ2009); a nested static class outside
a component had no module, and one inside a component was its module's alone.

Every class, static class, record and struct declared inside another type is now a module named by
the types that contain it and its own name joined by `$` (`Cart$Item`, `A$B$C`), a name no C# type
can take: TwinTypeName, from a symbol or a declaration, is what the parser, the dependency resolver,
the emitters and every strategy that writes a type (a construction, a type test, a static member, an
operator, a zero, an annotation, a hydration map) read, and a reference imports the twin as it
imports any type's. One inside a server-only class, an exception or an attribute has none, as its
owner has none. A record's text keeps its C# name. The runtime's one nested type regenerates as
`CodeBlock$CodeMetrics`, its annotations typed, and the component identity test reads a `$` in a
twin's name rather than taking the owner's.

The 18 conformance cases through the module graph pass in both annotation modes, where on the class
branch all of them failed (EQ2009 for the record pair) or one of the component's two threw. Compiler
1615, Server 220, Web 941, Conformance 3739 and the runtime's 2050 are green, and the dashboard
sample's markdown, code and diff screens render their code blocks with no console error.
Copilot AI balanced review requested due to automatic review settings October 6, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Há falhas em argumentos nomeados do StringBuilder, -0 com cultura invariável e precisão de nint/nuint fora de 2^53.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Este PR alinha a formatação no navegador à cultura usada pelo servidor, cobrindo números, datas, troca de cultura e texto gerado pelo transpiler.

Changes:

  • Transporta NumberFormatInfo e DateTimeFormatInfo em todas as páginas.
  • Centraliza formatação cultural de números, datas, records e conversões textuais.
  • Adiciona especificações e testes de conformidade entre .NET e JavaScript.
File Description
docs/​DIAGNOSTICS.md Atualiza diagnósticos culturais.
docs/​LEDGER.md Registra a mudança.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​.openspec.yaml Metadados OpenSpec.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​design.md Documenta o design.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​proposal.md Arquiva a proposta.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​specs/​page-culture/​spec.md Delta da cultura da página.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​specs/​runtime-dates/​spec.md Delta de datas.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​specs/​transpiler-bcl/​spec.md Delta do transpiler.
openspec/​changes/​archive/​2026-10-06-a-number-and-a-date-print-in-the-pages-culture/​tasks.md Registra tarefas concluídas.
openspec/​specs/​page-culture/​spec.md Define transporte e uso da cultura.
openspec/​specs/​runtime-dates/​spec.md Define formatação dos tipos de data.
openspec/​specs/​transpiler-bcl/​spec.md Define conversões textuais culturais.
src/​eQuantic.Build/​Program.cs Remove metadados dos catálogos de strings.
src/​eQuantic.UI.Compiler/​CodeGen/​CSharpToJsConverter.cs Expõe geração textual de records.
src/​eQuantic.UI.Compiler/​CodeGen/​Eq.cs Substitui helpers numéricos.
src/​eQuantic.UI.Compiler/​CodeGen/​Extensions/​TypeSymbolExtensions.cs Identifica tipos de data.
src/​eQuantic.UI.Compiler/​CodeGen/​RecordTypeEmitter.cs Encaminha records ao formatador.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Expressions/​InterpolatedStringStrategy.cs Propaga o tipo numérico.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​FormatKind.cs Classifica tipos numéricos.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Invocation/​ToStringStrategy.cs Encaminha ToString cultural.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Primitives/​ConvertStrategy.cs Implementa Convert.ToString.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Primitives/​StringStaticStrategy.cs Propaga tipos em string.Format.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​StringConversion.cs Centraliza conversões para texto.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Types/​DateOnlyTimeOnlyStrategy.cs Formata DateOnly e TimeOnly.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Types/​DateTimeOffsetStrategy.cs Formata DateTimeOffset.
src/​eQuantic.UI.Compiler/​CodeGen/​Strategies/​Types/​StringBuilderStrategy.cs Formata valores do StringBuilder.
src/​eQuantic.UI.Compiler/​PublicAPI.Unshipped.txt Atualiza a API pública.
src/​eQuantic.UI.Components/​MermaidLayout.cs Mantém SVG em cultura invariável.
src/​eQuantic.UI.Runtime/​src/​eq.ts Exporta o novo helper numérico.
src/​eQuantic.UI.Runtime/​src/​shared/​__fixtures__/​NestedChild.ts Atualiza fixture gerada.
src/​eQuantic.UI.Runtime/​src/​shared/​__fixtures__/​NestedHost.ts Atualiza fixture gerada.
src/​eQuantic.UI.Runtime/​src/​shared/​__fixtures__/​SharedCounter.ts Atualiza fixture gerada.
src/​eQuantic.UI.Runtime/​src/​shared/​__fixtures__/​calendar-format.ts Monta dados de calendário para testes.
src/​eQuantic.UI.Runtime/​src/​shared/​__fixtures__/​format-subset.txt Atualiza resultados de referência.
src/​eQuantic.UI.Runtime/​src/​shared/​calendar-names.spec.ts Testa nomes culturais.
src/​eQuantic.UI.Runtime/​src/​shared/​calendar-names.ts Lê nomes dos dados culturais.
src/​eQuantic.UI.Runtime/​src/​shared/​component-parity.spec.ts Atualiza paridade dos componentes.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​AccordionItem.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Badge.ts Formata contagens.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​BarChartGeometry.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​BarRect.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Calendar.ts Formata números do calendário.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CategoryAxis.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CellRef.ts Formata referências e record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​ChartSeries.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeBlock.ts Formata números de linha.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeCell.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeCollapse.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeCompletionItem.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDecoration.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDiagnostic.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDiff.ts Formata métricas e linhas.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDiffFold.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDiffGap.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeDiffLayout.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeEdit.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeEditor.ts Formata contagem de resultados.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeFiller.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeFold.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeGrid.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeGutterMarker.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeHover.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeInnerChange.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeLanguageRules.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeLineChange.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeMetrics.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodePatchFile.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodePatchHunk.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodePatchLine.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodePosition.ts Formata posição textual.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeRange.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeRow.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeRows.ts Formata mensagens numéricas.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeToken.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Crumb.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CultureOption.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​DataColumn.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​DataRow.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​DataTable.ts Formata chaves numéricas.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​DialogAction.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​FieldError.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​FieldRule.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​MarkdownParser.ts Formata sufixos numéricos.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​MenuItem.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​MermaidLayout.ts Emite coordenadas invariáveis.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​NavItem.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​PageIndicator.ts Formata rótulos de páginas.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Pagination.ts Formata números de páginas.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Rules.ts Formata mensagens de validação.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​SdkStrings.ts Usa o novo boxing numérico.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​SheetCellSnapshot.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​SheetRange.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Spreadsheet.ts Formata linhas da planilha.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​Stepper.ts Formata seu valor.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​ValueAxis.ts Formata valores e record.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​ValueTicks.ts Regenera texto do record.
src/​eQuantic.UI.Runtime/​src/​shared/​devices/​culture-controller.spec.ts Testa troca dos dados culturais.
src/​eQuantic.UI.Runtime/​src/​shared/​format-subset.spec.ts Compara o subset com .NET.
src/​eQuantic.UI.Runtime/​src/​utils/​culture.spec.ts Testa instalação e carregamento cultural.
src/​eQuantic.UI.Runtime/​src/​utils/​culture.ts Armazena formatos culturais ativos.
src/​eQuantic.UI.Runtime/​src/​utils/​dateonly-parse.spec.ts Atualiza testes de parsing cultural.
src/​eQuantic.UI.Runtime/​src/​utils/​datetime.ts Usa o novo formato de data.
src/​eQuantic.UI.Runtime/​src/​utils/​format.spec.ts Amplia testes do formatador.
src/​eQuantic.UI.Runtime/​src/​utils/​format.ts Implementa formatação numérica e temporal.
src/​eQuantic.UI.Sdk/​Resources/​boot.ts Instala os dados antes da hidratação.
src/​eQuantic.UI.Server/​CultureBridge.cs Serializa cultura e catálogos.
src/​eQuantic.UI.Server/​UIExtensions.cs Expõe o documento cultural.
src/​eQuantic.UI.Web/​CultureFormatBridge.cs Transcreve dados culturais do .NET.
src/​eQuantic.UI.Web/​PublicAPI.Unshipped.txt Declara a nova API pública.
tests/​eQuantic.UI.Compiler.Tests/​CodeGen/​ThemeMethodCasingTests.cs Atualiza emissão esperada.
tests/​eQuantic.UI.Compiler.Tests/​Coverage/​bcl-surface.baseline.txt Atualiza cobertura da BCL.
tests/​eQuantic.UI.Compiler.Tests/​Coverage/​diagnostics.baseline.txt Remove o diagnóstico aposentado.
tests/​eQuantic.UI.Compiler.Tests/​CultureCrossingTests.cs Testa provedores culturais.
tests/​eQuantic.UI.Compiler.Tests/​Strategies/​AuthoringCoverageTests.cs Atualiza saídas do transpiler.
tests/​eQuantic.UI.Compiler.Tests/​Strategies/​StringStrategyTests.cs Atualiza boxing numérico esperado.
tests/​eQuantic.UI.Conformance.Tests/​CultureHarnessTests.cs Torna a cultura do harness determinística.
tests/​eQuantic.UI.Conformance.Tests/​DatePictureConformanceTests.cs Testa imagens personalizadas de data.
tests/​eQuantic.UI.Conformance.Tests/​DateTypeTextConformanceTests.cs Testa tipos adicionais de data.
tests/​eQuantic.UI.Conformance.Tests/​Infrastructure/​ConformanceRunner.cs Instala cultura no lado JavaScript.
tests/​eQuantic.UI.Conformance.Tests/​Infrastructure/​DotNetEvaluator.cs Executa o lado .NET na cultura indicada.
tests/​eQuantic.UI.Conformance.Tests/​Infrastructure/​InvariantByDefault.cs Define cultura invariável do assembly.
tests/​eQuantic.UI.Conformance.Tests/​NumberKindConformanceTests.cs Testa tipo e símbolos numéricos.
tests/​eQuantic.UI.Conformance.Tests/​NumberTextInTheCultureConformanceTests.cs Testa conversões numéricas para texto.
tests/​eQuantic.UI.Conformance.Tests/​StandardNumberLayoutConformanceTests.cs Testa layouts N/F/C/P.
tests/​eQuantic.UI.Server.Tests/​Budgets/​served-runtime.json Atualiza o orçamento do bundle.
tests/​eQuantic.UI.Server.Tests/​CultureBridgeTests.cs Testa shell e endpoint culturais.
tests/​eQuantic.UI.Web.Tests/​FormatSubsetTests.cs Gera dados de referência do .NET.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/eQuantic.UI.Compiler/CodeGen/Strategies/FormatKind.cs
Comment thread src/eQuantic.UI.Compiler/CodeGen/Strategies/Types/StringBuilderStrategy.cs Outdated
…ports the name it calls

The review of this branch found the server on the other side of the rule.

- The server named the page it serves by the CLR simple name, in the route table and in the shell's
  configuration, while eqc names a nested component's module by its owner (`Admin$Users`), so a page
  declared inside a class asked for a module nothing writes. One rule, `TwinName.OfType`, in the file
  the compiler and the server already share, names both, and the server's tests read their own nested
  pages by it.
- A static method reached bare and a conversion operator registered a runtime type's import by its
  simple name and called it by its twin, so a nested runtime type was imported under one name and
  called by another. A reference writes one name now, `TwinReference`: a nested type's twin where eqc
  writes it, and the simple name a top-level type and a hand-written runtime twin keep.
- The identity check over the shared library's twins keys them by that name, a nested component
  included, where it skipped every name with a `$`.
- The `[ServerOnly]` check has one home, and `SymbolOf` has its doc comment back.
…nded to it, print in the culture

The builder converted its value only when every argument was positional, so
`sb.Append(value: -1.5)` reached the runtime as a number and `Append(value: null)` wrote `null`.
The value is now the argument the bound tree binds to the `value` parameter, and the call passes
every argument where C# binds it, each evaluated once in the order it is written
(`BoundArguments.Call`, the receiver first), so `Insert(value: V(), index: I())` runs V before I.

An interpolated string handed to `Append` or `AppendLine` builds StringBuilder's own handler, and
the bound tree shows its plain hole as the argument of the handler's AppendFormatted rather than as
an interpolation. ValueFlow now reads that shape as the same hole, so `sb.Append($"{d}")` writes
`1,5` and `True` on a pt-BR page, as the server does.
…nned

`half.ToString(CultureInfo.InvariantCulture)` with `half = -1 / 2` writes `String(half)`, and
JavaScript's String(-0) is "0", as .NET's text is. The negative zero case now writes that shape
too, so it runs on both sides in five cultures.
…to it, in the spec

The transpiler-bcl requirement for a number's text names the builder's value written by position
or by name, and an interpolated string handed to one, with a scenario for each, in the main spec and
in the archived change it came from. The ledger line counts the new case: 187 of the 222
conformance cases fail on main.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

A mudança atravessa 129 arquivos e regras culturais sensíveis, exigindo validação humana final apesar da cobertura abrangente e de nenhum defeito concreto encontrado.

Review effort: Balanced
Findings: None

Resolved since last review (3)

Main brought #618 (the code engine's completion), #628 (a target's 24dp floor) and #607 (a char's
search). Two files conflicted where main moved what an annotation crosses as into TsStandIn: a
member's type (TypeDeclarationExtensions) and a local's (LocalDeclarationStrategy). Each now asks
the stand-in first, then names a nested type by its twin, as this branch did, with main's OrNull for
a nullable one. The ledger line moves after the entries main brought.
…its full name

A plain class's twin wrote JavaScript's `[object Object]` as its text, and a plain struct's a
record's (`Pt { X = 0 }`), where .NET's Object.ToString and ValueType.ToString write the type's full
name, its namespace and the types it is nested in joined by `+` (`My.App.Outer+In`). DefaultText
answers that name for a type that declares no ToString and derives none from a base the app
declares, and the class and the struct emitters write it. A record keeps its synthesized text, an
exception its message's, and a generic type is left as it was: its name carries type arguments the
browser does not hold.

Forty-one of the runtime's own types write their name now, which the twins carry, and the served
runtime grows 389 bytes gzipped (195,582 to 195,971). With the record half #454 already changed,
this closes #570.
A list's construction is one array, as C# builds it: what the
constructor copies, spread, then each element of the initializer in
order. A capacity with an initializer dropped the elements, a source
with one was joined to them by a comma (`source, [3]`), and the
target-typed form kept the elements and dropped the source, or built an
empty list from a source with no initializer at all (#564).
ToDictionary, ToLookup, GroupBy and Distinct hand their comparer to the
fence every collection's construction passes (EQ2007): one that asks
for the key type's own equality, ordinal strings or null is dropped,
since the lowering already finds the keys that way, and any other is
refused. ToDictionary refused every comparer, StringComparer.Ordinal
included (EQ1004), GroupBy refused every one too (EQ2008), ToLookup
called a comparer as an element selector or had no form after one, and
Distinct dropped whatever it was handed (#578).
A call bound to ICollection<T>'s own Add or Clear goes through the
runtime, which asks the value what it is, as Contains, Remove and
CopyTo already did: an array appends or empties in place, a set adds a
value it lacks, a linked list adds last, a dictionary adds the pair and
refuses a key already there in its own words, and a twin calls its own
add and clear. They were an array's push and splice, which a HashSet, a
LinkedList, a SortedSet and a dictionary's pairs lack, and the same
push was what a member's collection initializer added through (#593).

The runtime's count reads a twin's own count before walking it, so a
twin of the app's own behind the interface counts itself.
A JavaScript class chain has one member per name, and a method on a
derived prototype IS the override of its base's. So a method that hides
an inherited member, with `new` or with the same signature and no
`override`, was refused by the build (EQ1007) where its base was the
app's, legal C# and all, and answered for the hidden one where it was
not: `public new int GetHashCode()` answered `((object)c).GetHashCode()`
and `HashCode.Combine(c)`, where .NET answers object's.

A method that hides a member now holds a name of its own on its twin
(TwinMethodName): the hidden member's name with `$` and how many hidden
slots of that name it stands over (`name$1`, `name$2`), which no C# name
can take. Its declaration is written under it, and every call, method
group, base call, null-conditional call, initializer's `Add` and
`Deconstruct` the model binds to it reaches it by it, so a call bound to
the hidden member keeps reaching the hidden one, the runtime's own
protocol included. An override fills the slot of the method it
overrides, whatever that one is named. Only a method this compilation
declares takes one: a type the runtime carries is reached by the names
its twin was written with.

- EQ1007 compares the names the twin holds, so a method that hides one
  is not refused, and one that hides one and answers an interface is,
  since a call through the interface reaches the hidden one's name.
- A `ToString()` that hides object's is called as the app's own method.

Conformance cases through the module graph: a call through the base, the
hiding type and a base call of each, an override of a `new virtual`, a
method group, a null-conditional call, a hiding without `new`, a static,
a method over a field, a generic base, and GetHashCode and ToString.
…tEnumerator() says (#612)

A `foreach`, a spread, `string.Join` and every LINQ operator reach a
sequence in the browser through JavaScript's iteration,
`[Symbol.iterator]`, which no twin of the app's wrote, so each threw over
a class that implements `IEnumerable<T>`. And the twin held the type's
two `GetEnumerator()`s under one name: the explicit non-generic one was
written after the generic one, over it, and called itself.

- The twin of the type that declares the method its iteration goes
  through (its implementation of `IEnumerable<T>.GetEnumerator()`, or of
  the non-generic one where it implements no `IEnumerable<T>`) carries a
  `[Symbol.iterator]` that calls it, by the name it holds on the twin,
  and walks what it returns through `$eq.linq.iterate`: an enumerator an
  iterator method filled, or one the app wrote, by its `MoveNext` and
  `Current`, disposed when the walk ends, as a `foreach` disposes it. A
  derived type inherits it, and its override of the method answers. A
  class, a record and a struct alike (IterableTwin).
- An explicit implementation of the non-generic `IEnumerable` or
  `IEnumerator` member beside the generic interface's (`GetEnumerator()`,
  `Current`) is not written: the generic interface derives from the
  non-generic one, so the two answer alike by its contract, and the twin
  holds one name for them.

Public surface in `eQuantic.UI.Compiler`: `Eq.LinqIterate` is new. The
runtime's `$eq.linq` gains `iterate`.

Conformance cases through the module graph: `foreach`, `string.Join`,
LINQ, `ToList`, a spread and the non-generic `IEnumerable` over an
iterator method; an enumerator the app wrote, walked, under LINQ and
disposed by a loop that breaks; a struct's enumerator; an explicit
`IEnumerable<T>`; an abstract `GetEnumerator`; a class that implements
only `IEnumerable`; and a record's sequence.
An access through the indexer of IList<T>, IReadOnlyList<T> or IList
is a Place of its own kind, read and written by the runtime's
$eq.collections.item and setItem, which answer an array's subscript and
a twin's item and setItem alike, whichever the face holds when it runs.
Every writer takes it: the assignment, the compound, the step, the
coalescing write, the null-conditional read, an object initializer's
entry and a key from the end, which counts through $eq.collections.count.
A Count read through the face counts through the runtime too, and the
runtime's count reads a twin's own count. A receiver typed as an array
or a list keeps its subscript.

A twin read through the face read a JavaScript subscript and a length,
which a twin does not answer: `r[0] + r[2] + r.Count` over a twin was
null where .NET says 19 (#586).
…as-dotnet-does

The head of the queue, f03bcb6, holds #707, #654, #715, #752 and main at d7f61c0. Six files
conflicted, and each keeps both sides.

- IdentifierStrategy names a static's home by its twin (#654) and a method by the name it holds
  on its twin (#563).
- InvocationStrategy calls a static on its home's twin (#654) by the name the method holds there
  (#563).
- ObjectCreationStrategy builds a type by its twin's name (#654), with the types a generic
  exception class is built with (#611), inside the closing #689 puts around a generic record's.
- The transpiler-classes, transpiler-exceptions and transpiler-sequences specs keep the
  requirements of both sides, the queue's first.
- PlainClassModule gets back the using of ExceptionTypes the merge dropped, which #654's
  OwnerKeptOut reads.
- The ledger line moves back to the end of the events.

#654 keeps a type nested in an exception out, as an exception had no module before #611. The
next commit settles what the two say together.
Main is at #752's squash (7636da4), whose tree is the tree of the head this branch already merged (9e5a828): recorded with the ours strategy, nothing changes.
Main is at #752's squash (7636da4), whose tree is the tree of the head this branch already merged (9e5a828): recorded with the ours strategy, nothing changes.
…me's (#611)

A host with no project's compilation, a snippet compiled on its own, keeps an exception
class out by its base's name: the module rule has no chain to read. The construction
asked only the symbol, which the one-file model does have, so new Failure() named a twin
nothing wrote there and the page met "Failure is not defined", where main built the
runtime's exception. Found while merging over #654.

- ConversionContext.HasProjectCompilation says whether the host handed the project's
  compilation over, set by ComponentCompiler from the provider the parser asks, through
  the converter and the emitter. It is not the symbols' regime, which a host may demote
  while it holds the compilation.
- ExceptionTypes.HasTwin(type, context) answers a construction with it, in
  ExceptionCreationStrategy and ObjectCreationStrategy.
- ProjectProbe compiles a test's source with the project's compilation, as an SDK build
  does, and the tests of an exception class of the app's read it. ExceptionTypeTests pins
  the snippet compiled on its own.
…ception, are modules (#611)

#654 makes a class declared inside another type a module of its own, named by its owner,
and kept a type declared inside an exception out, as an exception had no module then.
#611 makes an exception class a class. Merged, the build wrote a module for an exception
declared in a class that nothing constructed, since such an exception was still built as
the runtime's, and refused (EQ2010) a record declared in an exception class it writes.

- ExceptionTypes.HasTwin takes an exception declared inside another type for the class it
  is, its twin named by its owner (Holder$Nested), unless a type it is declared in stays
  on the server or is the runtime's.
- PlainClassModule.OwnerKeptOut keeps out what an attribute or a server-only type
  declares, and no longer what an exception class does. The server's TwinName.OwnersCross
  routes a page by the same rule, and the messages of EQ2010 and of MapPage say it.
- Conformance: a nested exception's members and its typed catch, a record declared in an
  exception class, and an exception nested in a generic class caught by its construction.
- ExceptionTypeTests: the modules are written, an exception nested in a generic class
  built where its type argument is a type parameter is EQ2013 and never EQ2010, and a
  nested type of a server-only owner is still EQ2010.
…agnostics and the specs (#611)

- docs/DIAGNOSTICS.md: EQ2010 keeps out what a server-only type or an attribute declares,
  and a type declared in an exception class of the app's crosses as that class does.
- openspec: the nested type requirement of transpiler-classes says an exception class is
  an owner as any class is, with its scenario, as a MODIFIED requirement of the archived
  change, and the exception class requirement no longer leaves a nested one out. The
  design and the tasks record what #654 and this change say together, and the snippet
  compiled on its own.
219,741 bytes gzipped, 500 over the 219,241 the queue's head records: the exception base
and its construction, and the walk of an enumerator the app wrote. The twins and the BCL
baseline, regenerated over the merge, did not change.
…n call as it was measured

- docs/LEDGER.md: 66 of the 71 new conformance cases fail on the queue's head (f03bcb6),
  the 13 Compiler cases of the second review fail before it, and the served runtime grows
  500 gzipped bytes. An exception declared in a class, and what an exception class
  declares, are in the line.
- The call that ran another constructor is new Snap(info, context), its arguments held in
  locals: .NET answers "serialization" and the browser answered "strings". Written with
  new StreamingContext() in the call, the browser threw instead, since that type has no
  twin. docs/DIAGNOSTICS.md, the spec's scenario, the design and the two comments say the
  one that was measured.
…time-provided owner

Found by Copilot's review of #707, in this pull request's code. MapPage
refuses a page whose owner TwinName.OwnersCross refuses, and its message
listed [ServerOnly], an exception and an attribute as types that never
reach the browser. A [RuntimeProvided] owner is refused too, and it does
reach the browser, inside the runtime. The message names the four and
says what they share: the build writes no module for them.
An exception built or tested where a type argument it carries is a type parameter was
refused under EQ2013, which is a topic built over a type parameter. The diagnostics
baseline went red on it, as it should: a code that gains a reporting site has to be the
SAME error, and this one has another subject and another remedy. It is the erasure that
refuses new T() (EQ2003) and that topic (EQ2013), and each of those has a code of its own.

- ExceptionTypes.RefuseOpen reports EQ2014, and ExceptionTypeTests reads it.
- docs/DIAGNOSTICS.md gives EQ2014 its row and EQ2013 its own back, the baseline gains the
  one line, and the ledger, the specs, the design and the tasks name the code.
…-as-dotnet-does

d9c3e37, which names a runtime-provided owner in MapPage's refusal (found by Copilot's review of this pull request, in #654's code).
Found by Copilot's review of #707. The comment said an unmarked copy is not taken for another type, above an assertion that it compares as the marked one: an unmarked value compares as any closed type, and only two marked differently differ.
edgarmesquita added a commit that referenced this pull request Oct 11, 2026
…he translated surface (#715)

Closes #569

## What

One measurement instead of one fix per report.
`NullArgumentConformanceTests` calls every member of the BCL surface eqc
translates with a null for each of its reference parameters, on .NET and
in the browser, and compares what the call did: whether it threw and the
exception's type, its `ParamName`, its message where the SDK composes
it, and the value a call that returned answered, a string, a bool, a
char, a number or an array of those.

- The surface is derived, never listed. Its types are the ones the BCL
audit's committed record names (`bcl-surface.baseline.txt`), its members
are what reflection finds on them, constructors, indexers and generic
methods included, and eqc's own diagnostics say which of them it
translates. A guard fails where a member the audit grades `native` or
`eq` takes a reference parameter and no null reaches it on both sides.
- Each probe runs a control, the same call with the null replaced by a
canonical value. The canonical text holds white space around and inside
it, which is what a null separator or a null set of trimmed characters
stands for in .NET. Where .NET answers the control and the browser
answers it otherwise, the member differs whatever the argument, and the
gap is recorded as `control` rather than charged to the null. A probe
whose other arguments .NET refuses on their own measures those
arguments, and is counted rather than compared.
- The gaps left are `null-argument-gaps.baseline.txt`, which may only
shrink: a new gap fails, an entry whose aspect moves fails, and an entry
that starts to agree fails until it is removed. Each entry carries its
aspect and a reason, and the file is regenerated with
`EQ_UPDATE_NULL_ARGUMENT_BASELINE=1`.
- The twins that read a null through null refuse it by name now, as .NET
does: LINQ's `Max`, `Min` and `ToDictionary`, a sequence a LINQ operator
reads through `seq`, named by the parameter it binds to (`second`,
`inner`, a `Zip`'s `first`) where every one was `source`,
`CompareTo(object)`, which answers 1 for a null, `new Guid(text)`, which
names its text `g`, `char.GetUnicodeCategory(text, i)`, and the
cancellation pair's `Register` and `CreateLinkedTokenSource`.

For someone using the SDK: a null handed to one of those members is
caught by `catch (ArgumentNullException e)`, and `e.ParamName` names
.NET's parameter. Nothing an app writes changes. The public surface
gains `Eq.GuidOf`, the developer surface does not move, and the
transpiled twins that concatenate sequences are regenerated: their `seq`
calls name `first` and `second`.

## Why

A .NET method that cannot take a null refuses it with an
`ArgumentNullException` naming the parameter. Several twins read the
argument first, so the browser threw a `TypeError`, which the runtime
reads as a `NullReferenceException`, and since #561 a typed catch lets
that through. #561 fixed the seven its review named, by hand, and the
rest of the surface had never been looked at.

What the measurement leaves, 402 probes, each with its reason:

- 213 are members lowered to JavaScript's own method: LINQ over a list
(168), an array's or a list's method, a string's search, a spread. They
read a null through null, and refusing it by name means a guard at every
call. That is a design decision, so I left it out of this pull request.
- 84 are provider overloads that eqc writes with the name `CultureInfo`,
which the runtime does not export, so the call is a ReferenceError in
the browser and nothing fences it at build time. eqc reads a culture
only where it formats or parses a number, and a culture held in a local
fails the same way.
- 26 are `Convert` of an object or of a string (#401), 21 are members
the runtime does not have (most of the sorted set's set operations,
`CopyTo` on the queue, the stack and the sorted set, and `TryParse` of a
`DateTime`, a `DateTimeOffset`, a `TimeOnly` and a `TimeSpan`), 6 are
collection constructors whose twin reads a null as no collection, and 3
drop a culture or a calendar argument.
- The other 49 are lowerings that are wrong whatever the argument, found
on the way: `Convert.ToBase64String` and `ToHexString` answer `String()`
of the array, `FirstOrDefault(defaultValue)` lowers the default as a
predicate, the `Zip` of three sequences lowers as the one with a
selector, `SequenceEqual` with a comparer answers its first sequence,
`ToLookup` with a comparer reads it as an element selector, `Average`
over longs adds a BigInt to a number, `LastIndexOf(value, start, count)`
ignores its count and `LastIndexOf(value, start)` reads its start as
JavaScript does (#724), six `String.Split` overloads hand their
separators to JavaScript's `split` whole (#753), `new decimal(bits)` and
`new Guid(bytes)` name classes the runtime does not export.

The list was 443 when this pull request opened. Main moved under it
since: #748 gave the builder its members, which took the 22 probes of
#679 out and left two whole-array `AppendFormat` ones among the
lowerings, and #660 refuses a constructor that takes a calendar at build
time, 8 probes.

Some cheap fixes stay in the list on purpose, because their files are in
open pull requests: the collection constructors and the `Contains` and
`Zip` twins (#707), `string.Format` and `string.Concat` with a whole
array (#637), and the `ToHashSet` and `ToDictionary` shapes (#707).

## OpenSpec

`a-null-argument-is-refused-as-dotnet-refuses-it`, archived, adds a
requirement to `transpiler-exceptions`: a null argument is answered as
.NET answers it, measured over the translated surface.

## Proof

The surface is 30 types and 823 members, 1205 probes, each a member and
the parameter handed null: 874 are compared on their null, 32 are
counted because .NET refuses their other arguments, 293 are members eqc
refuses at build time, 6 bind another overload as written and are left
out, and 250 need an argument no canonical value can write. Every one of
the 256 lines the audit grades `native` or `eq` that take a reference
parameter is reached.

A/B against main (7636da4), with main's version of every source file
this changes, built with 0 errors: `NullArgumentConformanceTests` fails
17 of its 26 tests there. The theory fails with exactly the 67 probes
the fixes close, and 13 of the 14 cases one canonical value cannot reach
fail too (a negative or false receiver of `CompareTo(object)`, a set
handed to `Concat`, a sequence as a `Zip`'s receiver, a null selector
caught by a typed catch). And 3 of the 7 cases of a receiver with an
effect fail there for the answer itself, a double's, a char's and a
bool's.

The 14th is the control that a value still compares, and the four checks
of the instrument pass on both sides. On this branch, 26 of 26 pass. The
fixes took the baseline from 510 entries to 443, and it holds 402 now,
for what main and the wider measurement moved since.

Copilot's first round found two. A receiver of `CompareTo(object)` that
its comparison reads once never ran when the argument was a null
literal, the null test answering before it reached the receiver. Every
`CompareTo(object)` reads its receiver twice now, which makes the writer
bind a receiver with an effect and run it first. With the strategy as it
was before that round, 3 of the 7 cases of
`TheReceiverOfCompareToObject_RunsBeforeItsNullIsRead` fail, an int's, a
double's and a char's receiver, and here the 7 pass. And the measurement
compared no array and its canonical text held no white space, so a split
by null passed whatever the browser made of it. It compares an array of
strings, chars, bools or numbers now, over a text that holds white
space, and that is what found the six `String.Split` probes (#753) and
moved `LastIndexOf(value, start)` to `control` (#724).

Each suite alone, read by its exit code and its total, on this branch's
head, a tree the merge of main 7636da4 left as it was: Compiler 1841,
Web 1021 against this branch's wiki page, Server 367, the runtime's 2316
in 170 files (`tsc` and vitest through the embedded bun), and
Conformance 4731 with 1 skipped. No run aborted.

The served runtime is 218,583 gzipped bytes, 183 more than the 218,400
main serves, measured the same way.

## Checklist

- [x] `dotnet test` on the affected test projects, and `dotnet build
src/eQuantic.UI.Runtime -t:TestRuntime`
- [ ] This PR's checks include a run named CI with jobs in it
- [x] `./scripts/check-openspec.sh` passes: the change validates
strictly and is archived
- [x] `dotnet build samples/DefaultUIDashboard`
- [x] A broken contract has a line in the migration notes: none is
broken
- [x] The wiki's SupportedFeatures page in English and Portuguese, on
the wiki branch `fix/a-null-argument-is-refused-as-dotnet-refuses-it`
- [x] One `docs/LEDGER.md` line for this event, citing #569
- [x] The diff reviewed by its author before the PR opened, and
Copilot's first round answered: two findings, both fixed
- [x] No "widget" in prose

---------

Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
…as-dotnet-does

The queue's head moved to 5e5b5ca: main with #752 squashed, and #654's refusal of a page
inside a type with no module, which names a runtime-provided owner. One file conflicted.

- UIExtensions.MapPage keeps both: the message names every owner the build writes no
  module for, [ServerOnly], [RuntimeProvided] or an attribute, as #654 writes it, and no
  longer an exception, whose class has a module of its own (#611).
#715 landed on main as 7d3018c, a squash of a head this branch already holds through the
queue's. Recorded with ours, so the tree does not move: a real merge changes three files
only, and each is already at the queue's later state here. The null-argument baseline is
without ToLookup's null comparer, which #707 closes, the served runtime's record is this
branch's, and the exceptions spec holds main's requirement and this change's.
Merges main 7d3018c, the first of the steps that bring this branch to the head of the queue. It
carries #741, #752 and #715, and none of them conflicts with this branch's source. #752 marks a
copy's closed type in the record twin this branch writes the text of, and #715 changes the
conformance harness beside this branch's culture install, and both merged as they stand.

docs/LEDGER.md and the PublicAPI files merged as a union, and this branch's ledger line moves back
to the end of the events. The twins and the baselines are regenerated after the last step.
Merges d9c3e37, the head of #654 as the queue's head holds it, the second of the steps that bring
this branch to the head of the queue.

How each source conflict was resolved:

- RecordTypeEmitter.cs: #654 names a nested record's twin by its owner (CodeBlock$CodeMetrics) and
  prints its text under its C# name, printedName, in the template its toString was. This branch
  had replaced that template with the runtime's record text, which writes each member in the
  culture. The branch's call stays and takes #654's printedName where it took the twin's name, so a
  nested record's text is still its C# name. A plain struct's full name is unchanged: it is .NET's
  name for the type, which already writes its owner.
- The twin CodeMetrics.ts, which this branch had regenerated, is the file #654 renamed to
  CodeBlock$CodeMetrics.ts: it goes, and CodeBlock.ts takes #654's side. Every twin is regenerated
  after the last step.
- served-runtime.json takes #654's number, and is regenerated after the last step.

docs/LEDGER.md merged as a union, and this branch's line moves back to the end of the events.
Merges 5e5b5ca, the head of #707 as the queue's head holds it, the third of the steps that bring
this branch to the head of the queue. No source file conflicts: #707's collection constructors,
comparers and LINQ table sit beside this branch's formatter, and DIAGNOSTICS.md and its baseline
merged on their own, with EQ2109 still retired and EQ2110 still this branch's.

Six runtime twins conflict and take #707's side. Every twin is regenerated after the last step.
docs/LEDGER.md and the PublicAPI files merged as a union, and this branch's ledger line moves back
to the end of the events.
…rows as .NET does

Merges 25643d5, the head of #708 and of the queue, the last of the steps that bring this branch
to it. No source file conflicts: an app's exception class becomes a twin over the browser's Error,
which this branch's default text already leaves alone (an exception's text is its message's), and
a sequence and a hiding method sit beside this branch's formatter.

served-runtime.json takes #708's record and is regenerated in the commits after this, with the
twins, the BCL baseline and the null-argument baseline. docs/LEDGER.md and the PublicAPI files
merged as a union, and this branch's ledger line moves back to the end of the events.
…ne over the queue's head

Regenerated from the merged compiler and runtime.

- The eight twins the merges took from the queue's side (EQ_UPDATE_TRANSPILED=1): their numbers say
  their kind again, and CodeBlock$CodeMetrics writes its text through the runtime's record text
  under its C# name, CodeMetrics. tsc is clean over them. The BCL baseline needed no change.
- The served runtime's budget: 223,575 gzipped bytes, against the 219,741 the queue's head serves
  (25643d5, built and measured the same way), 3,834 more, 1.74%: the layout of N, F, C and P and
  the date types' formatter, the twins' kinds, and the types that write their full name.
- The null-argument baseline (#715) loses 10 of its 401 entries and gains none. Seven now answer
  as .NET does: Convert.ToString of an object and of a string, with and without a provider, and a
  null format handed to DateOnly, TimeOnly and DateTimeOffset.ToString(format, provider). Three are
  probes eqc now refuses at the build with EQ2108, a provider held in a variable, as it already
  refuses DateTime's: Convert.ToString(bool, provider), DateTimeOffset.ToString(format, provider)
  and TimeOnly.ToString(format, provider).
- Two reasons no longer said what the measurement shows, and are rewritten by hand. The 79 entries
  left under "the provider is written as the name CultureInfo" said eqc reads a culture only where
  it formats or parses a number: it formats a date in one now, and what is left is parsing a date
  or a time span, writing a time span and listing a date's formats. The 18 Convert entries of an
  object named a String() lowering that left with Convert.ToString: they are a rounding or a
  comparison.
Main is at #654's squash (d7a8c31), whose tree is the tree of the head this branch already merged (d9c3e37): recorded with the ours strategy, nothing changes.
…s it from the runtime

Found by Copilot's third review of #708, in code that came with #654. A
static reached through `using static` introduces its declaring class,
and the import's bucket was read from the namespace alone. A nested type
of a [RuntimeProvided] owner in an app's namespace went to the app's
modules, which hold none of that name, so the call reached an undefined
`Kit$Part`. The bucket is the symbol's now, its owners included, as
every other path reads it.
Main is at #707's squash (178497c), whose tree is the tree of the head this branch already merged (5e5b5ca): recorded with the ours strategy, nothing changes.
…head

Eighteen pins of the compiler suite read the shape one side of the merge wrote, and take the
merged one.

- StringBuilderStrategyTests (#748) pinned a builder's interpolated append, AppendFormat and
  Append(object) as they were before the culture: a number's hole and a concatenated number write
  $eq.text.format with their kind, a format's values are boxed with $eq.text.asNumber, and a
  builder typed as an object is appended as its text. Eight cases, each read against what the
  merged compiler emits.
- CultureCrossingTests matched a date's construction as $eq.time.dateTime(, and #660 builds a
  DateTime and a DateTimeOffset through a factory, $eq.time.dateTime.of(. Its pattern takes both,
  for the ten cases that build one.
Merges 78215e9, the head of #708 after #654 and #707 landed on main (d7a8c31, 178497c). Over the
25643d5 this branch already held it brings one commit, 121a943: InvocationStrategy buckets a bare
`using static` call's class by its symbol, with its test in NestedTwinNameTests. The rest are
merges of main's squashes that change no content. Nothing conflicts, and no twin moves.
Records #708's squash, main 98c2b18, with the ours strategy. Its tree is the tree of 78215e9,
#708's head, which this branch already merged (8833abd on both), so this merge changes nothing:
it makes main an ancestor, and the difference from main is this branch's own files.
The line's numbers were measured against main at dc4a410. Against main as it is with #660, #748
and the queue landed (98c2b18's tree), 176 of the 222 conformance cases fail there and 46 already
held, where it was 187 and 35: #660 taught a date's picture its offset and #748 a builder's
interpolated holes their text since. The served runtime grows 3,834 gzipped bytes, 223,575 against
the 219,741 main serves, and ten entries leave #715's null-argument baseline.
@edgarmesquita
edgarmesquita merged commit 9305100 into main Oct 11, 2026
22 checks passed
@edgarmesquita edgarmesquita mentioned this pull request Oct 11, 2026
5 of 6 tasks
edgarmesquita added a commit that referenced this pull request Oct 11, 2026
Closes #756

The bump to `0.2.0-preview.61`, cut from main at 9305100 (#637) at
Edgar's request. It carries forty-five pull requests since `.60`: a
component that hears the server on a typed topic (#647), a page that
requires its authorization and a Server Action that refuses another
site's request (#686, #737), a layout that follows its box and type that
follows the window (#648, #665), a control's press and focus on every
target with a 24dp floor under a pointer (#617, #628), the code editor's
completion (#618, #653, #733), a hot reload under `dotnet watch` that
keeps the page's state (#666, #672), the image optimizer on SkiaSharp
(#730), the places where the producers drew one tree differently, with
Photon's hit-testing and its text layout (#688, #690, #699, #739, #741,
#749), a number and a date printed in the page's culture (#637), and a
long family of browser answers made .NET's (#579, #588, #594, #607,
#608, #621, #654, #656, #660, #661, #667, #668, #689, #696, #697, #705,
#707, #708, #709, #715, #731, #748, #752), with a page that answers HEAD
(#610) and the conformance harness's own comparison (#609).

## What this commit does

- `Directory.Build.props`: `0.2.0-preview.60` → `0.2.0-preview.61`.
- `./scripts/public-api.sh ship`: nine projects, 462 declarations join
the shipped surface and 43 retire, and every `Unshipped` is empty
afterwards. Checked after the fold, per project and compared as sets:
the lines that left `Shipped` are exactly the 43 `*REMOVED*` lines
declared on main, and the lines that joined are exactly its 462
declarations, so an orphan could not vanish unnoticed. The line diff
reads +463 −549: the version line, 462 lines into `Shipped` and 43 out
of it, and the 505 that leave the `Unshipped` files.
- The analyzer accepts the fold: the solution builds with no RS0016 or
RS0017. And it read the files: one bogus declaration appended to an
`Unshipped` failed the build with RS0017.
- Against `v0.2.0-preview.60` the `Shipped` diff has those 43 lines gone
and no other, so no pull request rewrote a shipped line in place this
time, as #512 did in `.60`.

## What the retirements are

Read out of the `*REMOVED*` lines, each paired with the line that joined
under the same member and attributed by the commit that wrote it, not
from the log. Only `ServerActionHub` left with nothing in its place, and
the rest changed shape:

- `ServerActionHub` and its constructor (#647): the hub, its route
`/_equantic/hub` and the `AddSignalR()` call `AddUI` made are gone. An
app that injected `IHubContext<ServerActionHub>` publishes through
`IServerEventPublisher`, and one that maps hubs of its own calls
`AddSignalR()` itself.
- `ICodeCompletionProvider.CompleteAsync` (#618) takes a
`CodeCompletionContext` and answers a `CodeCompletionList`.
`CurlyBraceLanguage.Keywords` was the protected abstract set a language
overrode, and it is the public list a completion offers now.
- `Drawing`'s constructor, `Drawing.Width` and `UI.Drawing` (#648) hold
the width as a `SizeValue`. `new Drawing(art, 240)` converts, and a read
into a `float` becomes `drawing.Width.Value`.
- `ImageOptimizer()` and `BlurPlaceholderGenerator()` (#730) take the
`ImageOptimizationOptions`. `UseImageOptimization` registers both, so
only an app that built one with `new` meets it.
- `HitRegion` (#690) gains `Drawn` after `Bounds`, so building one and
deconstructing one both change. `CodeRegion` (#653), `SheetRegion` and
`TextRegion` (#690) gain optional members at the end, so only a
positional deconstruction changes.
- `ServerActionsMiddleware`'s constructor (#737) takes an
`IOptionsMonitor<ServerActionsOptions>`. `UseServerActions()` builds it,
so only an app that constructs the middleware itself meets it.
- Four signatures widened at the end by optional parameters, which
compiles as before from source: `UI.Positioned` by the four fractions
and the two shifts (#648), `UI.Pressable` by `canRequestFocus` (#653),
`WebRealizer.Lower` by `density` (#688) and `PhotonRealizer.Realize` by
`sizedLike` (#690).
- `eQuantic.UI.Compiler`, which no app references, 21 lines: eleven of
#661 (`JsTemplate`'s `Annotate`, with its constructor, its `Deconstruct`
and the factory that took it, and six LINQ strategies that crossed to
the IR and lost their text `Convert`), four of #608 (`ValueMember`,
`ComponentDependencyResolver`'s constructor and
`RecordTypeEmitter.CanEmit`), two of #637 (`Eq.AsInteger` and
`Eq.AsSingle`), two of #654 (`UserDefinedOperators.Binary` and `Unary`),
and `HashSetStrategy` (#594) and `RangeIndexerStrategy` (#707) crossing
to the IR.

One break is an addition, which no `*REMOVED*` line shows.
`CurlyBraceLanguage.ReservedWords` (#618) is a new abstract member, so a
language derived from it renames its `Keywords` override to
`ReservedWords`. I listed every member that joined an interface or an
abstract class already shipped, and it is the only one an implementer
has to write: `ICodeLanguage.Keywords` and
`ICodeCompletionProvider.ResolveAsync` have default implementations.

The additions are 118 in `eQuantic.UI.Code` (the completion), 116 in
`Primitives` (the server events vocabulary, `FluidSize`, `Positioned`'s
fractions and shifts, `BoxStyle.Pressed`, `GridTrack.AutoFill`,
`Touch.MinPointerTarget`), 98 in `Server` (server events, the origins a
Server Action allows), 86 in the compiler, 19 in `Native.Components`, 12
in `Web`, 7 in `Components`, 4 in `Native.Framework` and 2 in `Images`.

## The rest of the surface

`developer-surface.baseline.txt` gains two configuration sections since
`v0.2.0-preview.60`, `EQuantic:ServerActions` (#737) and
`EQuantic:ServerEvents` (#647), and loses nothing: no MSBuild property
or template parameter came or went.

What an app meets with no signature moving:

- Three new build errors. EQ1009 (#608, #621, #708) stops two
constructors that take the same number of arguments, in a class, a
record or a struct that reaches the browser, where the twin kept one and
a `new` that meant another ran it. EQ2014 (#708) stops an exception
built or tested where a type argument it carries is a type parameter.
EQ2013 (#647) stops a `ServerTopic<T>` built over a type parameter,
which only new code can meet.
- Refusals widened under existing codes: EQ1007 by a second indexer and
a sequence over two types (#707, #708), EQ2004 by a stored `Range`, a
method group over a .NET extension and a .NET collection's own
`GetEnumerator()` (#656, #707, #708), EQ2007 by a comparer handed to
`ToDictionary`, `ToLookup`, `GroupBy` or `Distinct` (#707), and EQ2010
by an extension method of a server-only class and a type nested in one
(#654, #705, #731).
- Two refusals a running app meets on the server: a Server Action called
from another origin answers 403 until the app lists that origin (#737),
and a class-level `[Authorize]` guards the page as well as its actions
(#686).
- `image/avif` in the optimizer's `Formats` stops the app at startup
(#730), and a `Spacer` of weight below 1 or a negative `Flexible` weight
throws where it is written (#741).
- EQ2109 is retired and EQ2110 is about reading a number now, since a
number written as text follows the page's culture on both sides (#637).

The rest are browser answers made .NET's and drawings made alike across
the producers, listed in the tag.

## The wiki's version marks

`WikiVersionMarkTests` compares a mark only once a tag holds its symbol,
so the `.61` tag is when this release's marks are judged for the first
time. Emulated before the tag, with the test's own pattern and lookup
and a `.61` at 9305100: 145 marks, no mismatch. The thirteen it judges
first resolve to `.61`, as written: `StartFraction`,
`GridTrack.AutoFill`, `canRequestFocus`, `DENSITY_COOKIE`, `Drawing`'s
`SizeValue` width, `WithPageAuthorization`, `AllowServerActionOrigins`,
`IServerEvents`, `MapGetAndHead`, `UseServerEvents`, `TwinIsData` on the
tokens, `BoxStyle.Pressed` and `ScrolledThreshold`.

## The tag's gate, rehearsed

`gate-templates` runs only on a `v*` tag, so a template that does not
build costs a release attempt. Rehearsed on main at 9305100, before
this branch: 50 packages packed at `1.0.0-dev`, the three web and the
four native template shapes scaffolded and built in Release against that
feed alone, with no warning and with every eQuantic package restored at
the version packed, the published `runtime.js` identical byte for byte
to the Server package's, and the dashboard template served, hydrated and
navigated in a browser with no console error.

## Not in this release

No pull request is open. Forty-eight bugs are, none of them a regression
against `.60`. Three issues came out of this stretch and wait for a
decision or their turn: #753 (`string.Split` with an array of
separators, a count or a null separator), #754 (an app's exception class
in a snippet compiled on its own) and #755 (whether generic code should
carry its type arguments at run time, which EQ2014 fences).

The release notes travel in the annotated tag. The wiki's Upgrading page
carries the distilled entry in both languages, on the wiki branch
`chore/0.2.0-preview.61`, which merges into the wiki's master when this
merges and before the tag. The `docs/LEDGER.md` line for the release
follows the tag, as `.60`'s did, because it names the commit the tag
lands on.

## Checklist

- [x] The diff reviewed by its author before the PR opened: a version
line and a mechanical fold, verified as sets above
- [x] `dotnet test` on this branch: Compiler 1898, Server 374,
Conformance 5087 with one skipped, Web 1021, and the runtime's own 2355
in 170 files, none aborted
- [ ] This PR's checks include a run named CI with jobs in it
- [x] `./scripts/public-api.sh ship` run, every `Unshipped` empty, the
retirements read from the diff
- [x] The documentation changed with the release: the Upgrading entry in
English and Portuguese, on the wiki branch `chore/0.2.0-preview.61`,
with the wiki guards passing against it (9 of 9, and the vocabulary
guard fails on that page when a word it refuses is written into it)
- [x] Copilot's rounds answered: one round, approval recommended with no
finding and no thread, so the loop ends there

Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants