Skip to content

Fix the build blocker and the four correctness bugs (#123, #106, #107, #108, #109, #111) - #124

Merged
matt-edmondson merged 3 commits into
mainfrom
claude/schema-issues-kwr4lk
Aug 27, 2026
Merged

Fix the build blocker and the four correctness bugs (#123, #106, #107, #108, #109, #111)#124
matt-edmondson merged 3 commits into
mainfrom
claude/schema-issues-kwr4lk

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Works through the build blocker, all four bug-labelled issues, and the two enhancements that depend on them. The larger editor and code-generation issues (#110, #112#122) are untouched and want their own PRs.

#123 — build fails on any SDK that satisfies global.json

Reproduced exactly as filed: on SDK 10.0.111 every build failed with CS9057 before compiling a line, because ktsu.Sdk 2.28.0's analyzers reference Roslyn 5.9.

The band-to-Roslyn mapping, read from dotnet/sdk release branches (eng/Version.Details.xml):

SDK band Roslyn
10.0.1xx 5.0.0
10.0.2xx 5.3.0
10.0.3xx 5.6.0
10.0.4xx 5.9.0

So the global.json floor moves to 10.0.400, which is what CI already resolves 10.0.x to. Anyone on an older 10.0.x now gets an actionable "SDK not found" instead of a compiler error mid-build. The requirement is documented in the README's Building section.

#106BaseType.Equals was inverted

The final comparison was != where it meant ==, so distinct instances of the same type compared unequal while Object("A") and Object("B") compared equal — and GetHashCode() hashed ToString(), breaking the equality/hash contract.

Equality is now structural per derived type through a protected virtual bool EqualsCore(BaseType) hook: Object compares ClassName, Enum compares EnumName, Array compares element type, container and key. Hashing has the matching shape — BaseType.GetHashCode combines the CLR type with a protected virtual GetHashCodeCore that derived types override alongside EqualsCore — so equality and hashing cannot drift apart. == / != operators are added.

The three existing equality tests only exercised the ReferenceEquals short-circuit or two different CLR types, so they are strengthened rather than merely kept passing: distinct-instance equality, per-type structural comparison, HashSet dedupe, the operators, and the object overload.

#107 — vector and color types inherited Object

Vector2/3/4 and ColorRGB/RGBA derived from Object, inheriting an always-empty ClassName. That made Validate() reject every schema using them, left DisplayName blank in the editor's type picker, and wrote a meaningless "className": "" into every schema file.

SystemObject is now rooted at BaseType, and both it and Vector are abstract — neither is registered as a [JsonDerivedType], so an instance of either could never have serialized. IsObject now means "references a user-defined class", which is what Schema.Validation and Array.IsKeyed already assumed.

The TypeSystemTests assertions that encoded the old classification are updated, as the issue called for.

#108 — "Open Externally" hardcoded explorer.exe

The menu item threw an unhandled Win32Exception on Linux and macOS and took the editor down with it. It now hands the path to the platform shell via UseShellExecute, with launch failures surfaced as a message popup. Verified against the real failure path — the dev container has no xdg-open, and the thrown Win32Exception is caught by the filter rather than escaping.

#109 — test suite ran against one of three published frameworks

Schema.Test now multi-targets net10.0;net9.0;net8.0.

Worth flagging: ktsu.Sdk pins test projects to a single framework and does it from Sdk.targets, which is imported after the project body — so editing TargetFrameworks in the csproj alone is silently stomped. The project takes the last word via explicit Sdk.props / Sdk.targets imports, which is a little unusual and is commented in place. This arguably belongs upstream in ktsu.Sdk; if you'd rather it live there, this part is easy to drop.

CI also installs the 8.0 and 9.0 runtimes so each test host can start. The documented framework list drops .NET 7.0, which no project in the solution has ever targeted.

#111 — validation coverage

Validate() now reports: empty class / enum / member / enum-value names (error), a member still typed None (warning), an array with no container (warning), an array whose container is outside the known vocabulary (warning), and a map container with no key to map by (error).

The container vocabulary was two string literals buried in TryGetCollectionElementType; it is now named on Array as VectorContainer, MapContainer and KnownContainers, used by both the reflection importer and the validator. Container stays deliberately open-ended, so an unrecognized name is a warning rather than an error.

Empty enum values can't be produced through TryAddValue (it rejects them), so that rule is tested through deserialization — the path a hand-edited schema file actually takes.

Also: member types were silently dropped on load

Not a filed issue — the new round-trip assertions exposed it. SchemaMember.Type has a private setter and lacked [JsonInclude], so System.Text.Json wrote it on save and ignored it on load. Every member of every loaded schema came back as None, which means saved .schema.json files were effectively losing all type information.

The existing round-trip tests only counted members and never checked their types, which is why this survived. [JsonInclude] is added (matching the pattern already used for the model's collections), and TestRoundtripWithAllTypes now asserts that each of the 14 built-in types survives the round trip.

Verification

173 tests pass on all three target frameworks (146 before this branch). CI is green, including SonarCloud's quality gate — 92.3% coverage on new code, 0 security hotspots, 0% duplication.

One caveat on how this was checked locally: the dev container has SDK 10.0.111 and its network policy blocks builds.dotnet.microsoft.com, so neither SDK 10.0.400 nor the .NET 8/9 runtimes could be installed there. Local builds used Microsoft.Net.Compilers.Toolset 5.9.0 to supply exactly the Roslyn that 10.0.400 ships, and the net8.0/net9.0 suites were run rolled forward onto the .NET 10 runtime. Both shims are local only — nothing in the diff depends on them — and CI has since exercised the real global.json floor and the real per-framework runtimes on both ubuntu and windows.

Closes #106, closes #107, closes #108, closes #109, closes #111, closes #123

claude added 3 commits August 27, 2026 02:34
Closes #123: raise the global.json SDK floor to 10.0.400 and document it.
ktsu.Sdk 2.28.0's analyzers are built against Roslyn 5.9, which ships in the
10.0.4xx feature band; on a 10.0.1xx SDK (Roslyn 5.0) every build failed with
CS9057 before compiling a line. The pinned floor turns that into an actionable
"SDK not found". Band-to-Roslyn mapping confirmed against dotnet/sdk release
branches: 1xx -> 5.0, 2xx -> 5.3, 3xx -> 5.6, 4xx -> 5.9.

Closes #106: BaseType.Equals compared ToString() with != where it meant ==, so
distinct instances of the same type compared unequal while Object("A") and
Object("B") compared equal, breaking the equality/hash contract. Equality is
now structural per derived type - Object on ClassName, Enum on EnumName, Array
on element type, container and key - via a protected EqualsCore hook, with
GetHashCode kept in step and == / != operators added.

Closes #107: Vector2/3/4 and ColorRGB/RGBA derived from Object, so they
inherited an always-empty ClassName. That made Validate() reject every schema
using them, rendered their DisplayName blank in the editor's type picker, and
wrote a meaningless "className" into every schema file. SystemObject is now
rooted at BaseType and both it and Vector are abstract, since neither is
registered as a [JsonDerivedType] and an instance of either could not
serialize. IsObject now means "references a user-defined class", which is what
the validator and Array.IsKeyed already assumed. The TypeSystemTests
assertions that encoded the old classification are updated.

Closes #109: multi-target Schema.Test to net10.0;net9.0;net8.0 so every
published framework is exercised. ktsu.Sdk pins test projects to a single
framework from its targets, so the project takes the last word via explicit
Sdk.props/Sdk.targets imports. CI now installs the 8.0 and 9.0 runtimes so
each test host can start. The documented framework list dropped .NET 7.0,
which no project has targeted.

Also fixes a serialization bug the new round-trip assertions exposed:
SchemaMember.Type has a private setter and lacked [JsonInclude], so
System.Text.Json wrote it on save and ignored it on load - every member of
every loaded schema came back as None. The existing round-trip tests only
counted members, never checked their types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT
Closes #108: "Open Externally" named explorer.exe directly, so on Linux and
macOS the menu item threw an unhandled Win32Exception and took the editor down.
It now hands the path to the platform shell via UseShellExecute, and a launch
failure - no shell association, a file removed since it was opened, or a
missing xdg-open - surfaces as a message popup instead of terminating the
process.

Closes #111: Validate() accepted several constructs that break any downstream
consumer. It now reports an empty class, enum, member, or enum value name as an
error; a member still typed None as a warning; an array with no container as a
warning; an array whose container is outside the vocabulary the library
produces as a warning; and a map container with no key to map by as an error.

The container vocabulary was previously two string literals inside
TryGetCollectionElementType. It is now named on Array as VectorContainer,
MapContainer and KnownContainers, and both the reflection importer and the
validator use it. Container stays deliberately open-ended, so an unrecognized
name is a warning rather than an error.

Empty enum values cannot be produced through TryAddValue, which rejects them,
so that rule is tested through deserialization - the path a hand-edited schema
file would actually take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT
SonarCloud's quality gate failed the PR on reliability of new code: S1206
("this class overrides GetHashCode and should therefore also override Equals")
fired on Object, Enum and Array, each of which overrode GetHashCode while
overriding EqualsCore rather than Equals itself.

Rather than add an Equals override to each just to satisfy the pairing, hashing
now has the same shape equality does: BaseType.GetHashCode combines the CLR
type with a protected virtual GetHashCodeCore, and the three stateful types
override that hook instead. Equality and hashing are now overridden through
matching hooks, so they cannot drift apart, and no derived type overrides one
of the pair without the other.

Verified with SonarAnalyzer.CSharp run locally: S1206 goes from 6 occurrences
to 0, and every other rule's count is unchanged, so nothing new was introduced.
173 tests still pass on net8.0, net9.0 and net10.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WmzGm9XniSoqaiVGDT6qT
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 9c2bf88 into main Aug 27, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/schema-issues-kwr4lk branch August 27, 2026 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment