Repository navigation
🐛 fix: a number and a date print in the page's culture - #637
Merged
edgarmesquita merged 147 commits intoOct 11, 2026
Merged
Conversation
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.
There was a problem hiding this comment.
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
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
NumberFormatInfoeDateTimeFormatInfoem 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.
…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.
This was referenced Oct 6, 2026
…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.
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.
7 of 9 tasks
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.
…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.
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.
…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.
…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.
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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.
NumberFormatInfoandDateTimeFormatInfo, into__EQ_CULTURE__.format, and a switch with no reload fetches the same document from/_equantic/culture/{name}.json. A page with no culture installed formats in the invariant culture, never in its host's locale. The string catalogs carry strings only.ToString(),Convert.ToString,string.Format("{0}"),string.Join, aStringBuilder(its value written by position or by name, or an interpolated string handed to it) and a record's text (A number's text follows the culture only through a specifier #454). An integer never prints-0, which JavaScript makes of-1 / 2.D,XandBas .NET does, anintand anuintare 64-bit integers, and the per mille sign, an exponent's signs and the percent sign are the culture's own.N,F,CandPare laid out from the culture's data, its patterns, symbols, separators, group sizes and default digits (N, F, C and P write what .NET writes, in every culture #634).Intllaid them out, and wrote ar-EG's own digits where .NET writes ASCII ones, a no-break space where .NET's currency pattern has a plain one, and two digits forNwhere .NET reads three on ICU.DateOnly,TimeOnlyandDateTimeOffsetprint through the formatter aDateTimeprints through, each with the specifiers its type takes (DateTimeOffset, DateOnly and TimeOnly ToString still skip the formatter #469), and a custom picture writes the culture's/and:, the offset forz,zzandzzz, and the era forg(A custom date picture writes the culture's separators and the offset and era tokens as letters #470).ToString, and derives none, writes its type's full name as .NET does (My.App.Outer+In), where the twin wrote[object Object]or a record's text (A record's or a struct's text writes its values as JavaScript does, and a struct prints the record's format #570). A record keeps its synthesized text, and a generic type is left as it was. 46 of the runtime's own types write their name now.EQ2109 is retired. The public surface of
eQuantic.UI.CompilerreplacesEq.AsIntegerandEq.AsSinglewithEq.AsNumberand gainsTypeSymbolExtensions.IsDate, andeQuantic.UI.WebgainsCultureFormatBridge. 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.5where the server wrote1,5in pt-BR, a currency with a no-break space where the server wrote a plain one, aDateOnlyin 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 createspage-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 extendstranspiler-bcl(a number's text with no specifier, a number's kind) andruntime-dates(the date types and a custom picture). Its design says what is not here: the culture a server action runs in (#580), aDateTimeOffset'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 (
tscand 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.
DateTime's kind. This branch had rewritten the date code around one moment per value, so the kind and the zone's offset moved into it: the round trip andKwriteZ, a local time's offset or nothing,zwrites the offset, andUconverts by the kind and stays inside the calendar. 🐛 fix: a date is built by its constructor and read in the browser's time zone #660's own cases pass on this branch's formatter.$eq.num.single. Here the culture's formatter writes it with the kindsingle, the same digits in the culture's symbols. Every binding the emitted code declares starts with a$, the record text's arrow included.AppendandInsertwrite goes through the culture's text in front of it. SoAppend(0.1f),Append(1e21),Append(-0.0),Append(1e-7)andAppend(Tone.Warm)write what .NET writes, the rows A value a builder appends prints as .NET prints it #750 lists, measured through this branch's served runtime. Its last row stays: an enum held as anobjectstill prints its key.ToStringmethod group. This branch had given the same text a descriptor per member. They are onerecordText(value, name, members)now, each member its name alone or its name with a kind or a function, andrecordTextGrouptakes the same list.Convert.ToStringof an object and of a string, with and without a provider, and a null format handed toDateOnly,TimeOnlyandDateTimeOffset.ToString(format, provider). Three are probes the build now refuses with EQ2108, a provider held in a variable, as it already refuses aDateTime's. Two reasons that no longer said what the measurement shows are rewritten by hand.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
AppendLinewrites 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/2026on its localization screen in pt-BR, byte for byte what the server rendered, with a plain space afterR$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.jsonand 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 teston the affected test projects, anddotnet build src/eQuantic.UI.Runtime -t:TestRuntimeif TypeScript changed (the embedded Bun runstsc, thenvitest run)CIwith 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 ofCI. While GitHub Actions has no credits, the same run is on eQuantic Space (eqs runs ls)./scripts/check-openspec.shpasses: the OpenSpec change validates strictly, and it is archived before the mergedotnet build samples/DefaultUIDashboard(andPhotonDesktop/WalletMobileif the native track changed): CI'ssamplesjob 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 yourslocale/pt-BR/<Page>-pt-BR.mdthere)docs/LEDGER.mdline for this event, citing the issue/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