Skip to content

Feat/zodsharp 3 blazor - #23

Merged
kieronlanning merged 24 commits into
mainfrom
feat/zodsharp-3-blazor
Nov 3, 2026
Merged

kieronlanning merged 24 commits into
mainfrom
feat/zodsharp-3-blazor

Conversation

@kieronlanning

Copy link
Copy Markdown
Contributor

No description provided.

…rp 3.0.0-prerelease.1

SourceGeneratorFramework 1.2.2 stops its framework assembly leaking into the build output of
projects that reference a generator as an analyzer, so the in-repo samples and tests no longer
carry Purview.SourceGeneratorFramework.dll in their bin folders.

Purview.ZodSharp 3 ships net10.0 and net11.0 only, so the two ZodSharp samples move from the
repository's 'All' set to 'Supported'. Every other ZodSharp consumer in the repository already
targeted 'Supported'. The runtime package does not depend on ZodSharp and keeps 'All'.

ZodSharp 3 breaks surfaced by the tests:

- built-in rules now report a structured origin and category, so the NonSentinel test asserts
  origin 'string' and category 'invalid_value' instead of no origin
- generated schemas gain ValidateAsync, so CA1849 flags the synchronous Validate in an async
  test; it now awaits ValidateAsync
The generated scalar converter read and wrote its underlying value through
JsonSerializer.Deserialize<T>(ref reader, options) and JsonSerializer.Serialize(writer, value,
options), and the complex converter round-tripped a generated {Name}JsonModel class the same way.
Those overloads are [RequiresUnreferencedCode]/[RequiresDynamicCode], so every trimmed or Native
AOT consumer - Blazor WebAssembly included - got IL2026/IL3050 from generated code, and the model
class needed metadata no source-generated context could list. Nothing in the repository analyzed
generated code under trimming, so it went unnoticed; the new packed-consumer gate found it.

The converters now go through ValueObjectJson, a new runtime helper:

- the options' own metadata when they have it, so converters, naming policies and number
  handling behave exactly as before
- the generated converter of a value-object-typed member, which a source-generated context in
  the declaring project cannot see because one generator never sees another's output
- the built-in System.Text.Json converter for a primitive the context never listed
- otherwise a NotSupportedException naming the type to add with [JsonSerializable]

The complex converter reads and writes members directly, honouring PropertyNamingPolicy,
PropertyNameCaseInsensitive and DefaultIgnoreCondition, skipping unknown members and defaulting
missing ones, as the model class did. The public {Name}JsonModel nested type is no longer emitted.
BuildSdk 1.2.0 rewrote the managed .editorconfig with its new version marker; the rule set is
unchanged. It added Blazor WebAssembly, Razor class library, BFF and Blazor host detection,
PurviewBrowserSafe with the PRBLZ0001 reference check, PDS0007-PDS0009 and the unmarked
.editorconfig warning. None of them applies to a project in this repository - there is no Blazor
project in the solution, and the packed-consumer gate builds its Blazor apps outside the SDK - and
the build produced no new warning. Skills.Testing 1.2.0 passes every skill unchanged.
…sembly and Blazor Server

Every in-repo build references the generator and runtime as projects, which exercises nothing of
what ships. Modelled on purview-dev/zodsharp's gate, `just smoke-packed-consumers` packs
Purview.ValueObjects to an isolated feed and NUGET_PACKAGES, guards Directory.Build.* and
Directory.Packages.props in a temp root, and builds consumers outside the repository from the
package alone, with Purview.ZodSharp 3:

- a Native AOT console over scalars, a complex value object, [ZodSchema] DTOs and
  System.Text.Json source generation, published with -warnaserror and run
- a Blazor WebAssembly app with a <ZodValidator> form and a <DataAnnotationsValidator /> form
  over a model of value objects, published trimmed and with RunAOTCompilation=true, served and
  run in a headless browser driven over the DevTools protocol
- a Blazor Server app whose statically rendered form is posted invalid and valid, and whose
  interactive page runs the same self-test over a live circuit
- a net8.0 console, so the runtime's oldest target framework keeps restoring and running

Every bin and publish folder is scanned for compiler-host assemblies, including the value
object generator and code fix, by scripts/assert-no-compiler-host-assemblies.ps1.

Purview.ZodSharp prerelease gaps are reported as known issues rather than failures
(-StrictKnownIssues fails on them), and the WebAssembly AOT publish leaves out the type-level
rule that crashes the Mono AOT compiler while a separate probe keeps checking it.

PackValidation gains ForbiddenContent rules mirroring ZodSharp's, and `just verify-pack-rules`
(scripts/test-pack-validation-rules.ps1) proves each of the 25 rules rejects a package.
… requests

Purview.ZodSharp 3.0.0-prerelease.1 and its Blazor package are built against the rc.3 ASP.NET
Core packages, and the WebAssembly consumers need the matching rc.3 WebAssembly SDK, so
global.json and both workflows move from rc.2 to rc.3 together.

The new packed-consumers job runs scripts/test-packed-consumers.ps1 on ubuntu-latest with
-WasmAot Require and -Browser Require (the image ships Chrome), then proves every pack-validation
ForbiddenContent rule. Purview.SourceGeneratorFramework 1.2.2 and Purview.ZodSharp
3.0.0-prerelease.1 are not on nuget.org yet, so until they are - or the
VALUEOBJECTS_DEPENDENCY_FEED repository variable names a feed that has them - the job fails at its
first step, the dependency preflight, with what to do, instead of skipping.
…grade

A new guide, docs/Trimming-AOT-and-Blazor.md, covers the target frameworks (the runtime keeps
net8.0-net11.0; ZodSharp 3 needs net10.0+), the generated JSON converters under trimming,
registering generated converters on a source-generated JsonSerializerContext declared in the
same project, binding a value object in a Blazor form, validating it with Purview.ZodSharp.Blazor,
and the Purview.ZodSharp 3.0.0-prerelease.1 gaps the packed-consumer gate found.

The README, the package README, the ZodSharp guide, both affected skills and AGENTS.md point at
it, and AGENTS.md names `just smoke-packed-consumers` as the proof that the generated path is
trim- and AOT-clean.
A minor prerelease: no target framework is dropped, and the public API only grows (ValueObjectJson).
The generated complex converter no longer emits its public {Name}JsonModel nested type, which is
generated into consumer code rather than shipped in the package.

The changelog's former "Unreleased" section shipped across 1.2.0-1.2.3, so it is now headed that way.
…field placement

Purview.ZodSharp 3.0.0-prerelease.1 now compiles a type-level [ZodRule] under the Mono AOT compiler
and places an error inside a value object on the owning member's field, so the gate stops
accommodating either.

The WebAssembly AOT publish builds the same model as every other publish, PlanId's [NonSentinel]
included; the EXCLUDE_TYPE_LEVEL_RULES define, the ExcludeTypeLevelRules property and the separate
crash probe are gone. The self-test's KNOWN lines are ordinary checks: each value object's message
sits on its own field and names it ("Field 'Email' must be a valid email address."), the type-level
rule's message sits on Plan, and no message names Value. The static Blazor Server post asserts the
same placement. The known-issue reporting and -StrictKnownIssues are removed, so every check fails
the gate.
…console

The Native AOT console's Order declared ContactEmail as a non-nullable EmailAddress only because
Purview.ZodSharp's generated null test for a [Required] nullable struct bound the scalar's
operator ==(string?, EmailAddress) and failed with CS9342. That is fixed, so the member is back to
its natural shape, [Required] EmailAddress?.

The console proves it compiles under -warnaserror and validates: a missing address is reported at
ContactEmail with RequiredRule's code, and a present but invalid one is reported at
ContactEmail.Value with a message that names the owning member.
The Trimming, Native AOT and Blazor guide, the CHANGELOG and the purview-value-objects-zodsharp
skill listed Purview.ZodSharp 3.0.0-prerelease.1 defects found by the packed-consumer gate. All of
them are fixed in ZodSharp, so the wording is gone. In its place the guide and the skill describe
the behaviour the gate now asserts: an error inside a value object is shown on the owning member's
field and named after it, a type-level rule is located at the member, and a [Required] nullable
value object member is supported. The upgrade notes record the changed message text.
The skills this repository depends on are installed by the skills CLI into the universal
.agents/skills/ location and committed with skills-lock.json, so a clone has them without
running anything. Nothing was written outside .agents/ and skills-lock.json:

    npx -y skills@1 add purview-dev/build-sdk -s '*' -a universal -y
    npx -y skills@1 add purview-dev/sourcegenerator-framework -s '*' -a universal -y
    npx -y skills@1 add purview-dev/zodsharp -s '*' -a universal -y

That is the eight purview-buildsdk-*, six purview-sourcegenerator-* and six purview-zodsharp-*
skills. This repository's own skills stay in skills/.

/.claude/ is gitignored, and src/Directory.Build.props sets PurviewSkillsAgents to universal, so
the SDK's InstallPurviewSkills target no longer defaults to writing a copy for every agent.
…ention

InstalledSkillTests checks that every skills-lock.json entry is installed under .agents/skills/,
that every installed folder is recorded in the lockfile, that no entry is one of this
repository's own skills, and that neither a .claude/skills/ copy nor a CLAUDE.md exists.
…ntion

AGENTS.md gains an "Installed skills (.agents/skills/)" section: the installed set and its
sources, the -a universal install and restore commands, why nothing goes into .claude/ and no
CLAUDE.md is added, and the test that guards it. The authored-skills section no longer says
.agents/skills/ is gitignored, and the layout table lists both skill folders.
Every purview repository installs purview-dev/engineering-playbook beside purview-dev/build-sdk.
Installed the same way as the others, into the universal location only, and nothing was written
outside .agents/ and skills-lock.json:

    npx -y skills@1 add purview-dev/engineering-playbook -s '*' -a universal -y

That adds purview-architecture, purview-architecture-testing, purview-engineering-principles,
purview-persistence, purview-telemetry and purview-testing-conventions.
InstalledSkillTests now mirrors ZodSharp's five checks. A skill authored in skills/ must not
appear in .agents/skills/, whether or not the lockfile records it, and the CLAUDE.md check also
covers .github/CLAUDE.md.
The installed-skills table and install commands include purview-dev/engineering-playbook, and the
description of InstalledSkillTests matches its checks.
The packed-consumer gate's Blazor WebAssembly consumer had no
wwwroot/index.html in the repository: the web section of .gitignore
ignores *.html at any depth, so the page existed only on the machine
that wrote it. A fresh CI checkout therefore published an app whose
wwwroot held _framework but no index.html, the static file host's
MapFallback threw FileNotFoundException sending it, and the gate failed
with "The WebAssembly host answered 500 for /".

Add the page, un-ignore the fixtures' wwwroot folders, and assert
index.html in Assert-WasmOutput so a future loss is reported as a
missing file rather than as a 500 from the host.
The pack-validation step failed with exit 1 because dotnet tool
restore reported

    The command "purview-build" specified in the tool manifest file is
    not contained in the package with Package Id purview.build. The
    commands contained in the package are dotnet-inspect.

although Purview.Build 0.8.1 declares <Command Name="purview-build">
and restores correctly on its own. Reproduced on a cold package cache
with both 0.8.1 and 0.7.1: the .NET 11 rc.3 CLI attributes the command
of the RID-specific dotnet-inspect tool to the manifest entry beside it.
The failed attempt has downloaded and installed every tool by then, so
the next attempt succeeds.

scripts/restore-dotnet-tools.ps1 retries a bounded number of times. It
is quiet on success and prints every attempt's output when all fail, so
the retry cannot hide a genuine failure. It is now the only place local
tools are restored: `just` (ensure-pipeline-tool), the CI
pack-validation step, and the scratch repository that
test-pack-validation-rules.ps1 builds for each of its 27 runs.

Also give pr.yml its final newline.
GitHub's pwsh shell appends `if ((Test-Path variable:\LASTEXITCODE)) {
exit $LASTEXITCODE }` to the step, so the step's result is whatever the
last native command left behind - and
scripts/test-pack-validation-rules.ps1 ends with 27 `dotnet
purview-build` runs that must fail, so the step reported exit 1 after
printing "Every ForbiddenContent rule (25) rejected its file".

Exit 0 explicitly once every rule has passed, and clear $LASTEXITCODE
when a retried dotnet tool restore succeeds, so the retry cannot poison
a step that only restores tools.

Reproduced with the runner's wrapper locally: cold cache, attempt 1
fails, attempt 2 restores, 27 validation runs pass, step exit 0.
@kieronlanning
kieronlanning merged commit b8d6049 into main Nov 3, 2026
2 checks passed
@kieronlanning
kieronlanning deleted the feat/zodsharp-3-blazor branch November 3, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant