Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43fd11a
test: added nonsentiel 'default' tests
kieronlanning Oct 9, 2026
7fe3f7a
fix: rules on properties (i.e. args) of records
kieronlanning Oct 9, 2026
5194d42
feat: scalar detected error messages now include the type name
kieronlanning Oct 10, 2026
d5226f1
refactor: true schema export for custom/ non-std zod rules
kieronlanning Oct 11, 2026
7016892
refactor: removed old bridge for specific rule handling
kieronlanning Oct 11, 2026
1a98ed1
feat: added uuid versions + guid support
kieronlanning Oct 11, 2026
38e5e2f
chore: bumped build-sdk
kieronlanning Oct 12, 2026
cfc0e37
test: dispose source generator results that emit an assembly
kieronlanning Oct 12, 2026
62411f5
chore: bumped psgf
kieronlanning Oct 12, 2026
db3360d
build: install Purview.Build as a .NET local tool
kieronlanning Oct 12, 2026
94862c7
build: declare RequiredCompilerFlags explicitly
kieronlanning Oct 13, 2026
0e1e9d9
build: take Purview.Build 0.5.3
kieronlanning Oct 13, 2026
cecd425
build: take .NET SDK 11.0.100-rc.2.26504.105
kieronlanning Oct 15, 2026
a8a0d7e
ci: take .NET SDK 11.0.100-rc.2.26504.105 in workflows
kieronlanning Oct 15, 2026
d190989
feat: migrate agent skills to the root skills/ folder and the skills CLI
kieronlanning Oct 15, 2026
48c6fde
chore: run CSharpier on save in Visual Studio
kieronlanning Oct 15, 2026
2325294
fix: updated to latest build-sdk + sgf
kieronlanning Oct 15, 2026
d34ff84
chore(release): name the 2.1.0 release and guard the public API
kieronlanning Oct 16, 2026
acd37c8
ci: run the Guid/UUID parity producer in the cross-platform job
kieronlanning Oct 16, 2026
ea9404c
test: drive the vitest cross-platform suites from the .NET test run
kieronlanning Oct 16, 2026
b4b5f82
test: run the vitest suites on bun or node, and never skip
kieronlanning Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
"rollForward": false
},
"dotnet-inspect": {
"version": "0.25.0",
"version": "0.26.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
"version": "0.5.4",
"commands": [
"purview-build"
],
"rollForward": false
}
}
}
6 changes: 5 additions & 1 deletion .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"TestFilter": "/*/*/*/*[Category=Unit]"
},
"PackValidation": {
// Explicit because Purview.Build 0.5.3 defaults RequiredCompilerFlags to empty. Earlier
// versions forced this check on and could not be configured off, so these packages have
// always been validated against it; declaring it keeps that behaviour.
"RequiredCompilerFlags": ["optimization=release"],
"RequireSymbolPackage": true,
"RequireSymbolFiles": true,
"RequiredContent": {
Expand Down Expand Up @@ -46,4 +50,4 @@
"Release": {
"Mode": "None"
}
}
}
23 changes: 21 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# install`. Before global.json pinned a version the same gap failed later instead, with
# NETSDK1045 on the net11.0 target, because the 10.0.x SDK cannot build it.
# Keep in sync with `sdk.version` in global.json.
dotnet-version: "11.0.100-rc.1.26425.128"
dotnet-version: "11.0.100-rc.2.26504.105"
run-pack: true
validate-pack: true
secrets: inherit
Expand Down Expand Up @@ -48,11 +48,30 @@ jobs:

- run: bun install --frozen-lockfile

- name: Run the C# cross-platform tests (writes the fixtures vitest reads)
# There are TWO producers of the fixtures vitest reads, in different assemblies, namespaces and
# classes, so they cannot be selected by one treenode filter:
#
# tests/ts/cross-platform.test.ts <- src/tests/cross-platform/output/
# ZodSharp.Json/*CrossPlatformTests (SystemTextJson and
# NewtonsoftJson unit tests)
# tests/ts/guid-uuid-parity.test.ts <- src/tests/cross-platform/guid-parity/
# ZodSharp.SourceGenerators/ZodSchemaGeneratorTests
#
# Both run as separate steps rather than one OR-ed filter: `a|b` is not honoured as a top-level
# alternation by the treenode filter — it silently selects a different, broader set and skips the
# parity producer, which is how the parity artifacts came to be missing in CI while this job stayed
# green. Keep a step per producer, and keep each filter anchored to its class.
- name: Run the C# cross-platform tests (writes cross-platform/output)
run: >-
dotnet test --solution src/ZodSharp.slnx --configuration Debug
--treenode-filter '/*/ZodSharp.Json/*CrossPlatformTests/*'

- name: Run the C# Guid/UUID parity test (writes cross-platform/guid-parity)
run: >-
dotnet test --solution src/ZodSharp.slnx --configuration Debug
--treenode-filter
'/*/ZodSharp.SourceGenerators/ZodSchemaGeneratorTests/JsonSchema_GivenGeneratedGuidSchemas_WritesZodParityArtifacts'

- name: Run the TypeScript/Zod cross-platform tests
run: bun run test

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
# The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins
# the .NET 11 SDK in global.json; without it the release job cannot resolve global.json.
# Keep in sync with `sdk.version` in global.json (and with .github/workflows/pr.yml).
dotnet-version: "11.0.100-rc.1.26425.128"
dotnet-version: "11.0.100-rc.2.26504.105"
release-mode: NuGet
release-branch: main
secrets: inherit
75 changes: 70 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ src/
cross-platform/ Output directory shared between the C# and TS cross-platform tests
src/ts/ TypeScript (Zod) schema + fixture generation; consumed by C# and vitest tests
tests/ts/ Vitest cross-platform tests
.agents/ Skills/prompts bundled from Purview.BuildSdk and Purview.SourceGeneratorFramework
skills/ Agent skills this repository authors and publishes
.agents/skills/ Skills installed from other repositories by the skills CLI (gitignored)
```

## Build system
Expand All @@ -46,7 +47,7 @@ The build is driven by `Purview.BuildSdk`, imported via `src/Directory.Build.pro
- `global.json` pins the `Purview.BuildSdk` MSBuild SDK version.
- Pack layout for all packages is validated by `purview-build.json` (`PackValidation.RequiredContent`).

Consult `.agents/skills/sdk-configuration-reference/SKILL.md`, `.agents/skills/sdk-project-behavior-and-detection/SKILL.md`, and `.agents/skills/project-placement-defaults/SKILL.md` before changing build/SDK configuration or adding/moving projects.
Before changing build/SDK configuration or adding/moving projects, install and consult the SDK's own skills: `npx skills add purview-dev/build-sdk` (`purview-buildsdk-configuration`, `purview-buildsdk-project-detection`, `purview-buildsdk-project-layout`).

## Commands

Expand All @@ -71,6 +72,8 @@ bun run test # vitest run (cross-platform TS tests)
bun run generate-fixtures # regenerates src/ts/fixtures/*.json from Zod
```

`just test` also runs the vitest suites through CliWrap (see [Cross-platform fixtures](#cross-platform-fixtures)), so `bun run test` is for iterating on the TypeScript side rather than the only way to exercise it. It needs `bun install` to have been run.

Formatting is enforced with CSharpier; `.editorconfig` at the repo root defines style (tabs for code, 2-space for XML/JSON/YAML/markdown).

## Testing
Expand All @@ -81,15 +84,15 @@ Formatting is enforced with CSharpier; `.editorconfig` at the repo root defines
- `// Arrange`, `// Act`, `// Assert` comments.
- Meaningful, descriptive method names (`Action_GivenCondition_ExpectedResult`).
- Treat work as incomplete until the relevant tests pass.
- For source generators, diagnostic analyzers, code fixes and refactorings, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`, `TUnitCodeFixTestBase`, `TUnitRefactoringTestBase`) and assert with `CodeQuery`. Load `.agents/skills/source-generator-testing/SKILL.md` and `.agents/skills/tunit-test-authoring/SKILL.md` before writing or changing these tests.
- For source generators, diagnostic analyzers, code fixes and refactorings, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`, `TUnitCodeFixTestBase`, `TUnitRefactoringTestBase`) and assert with `CodeQuery`. Install the framework's skills with `npx skills add purview-dev/sourcegenerator-framework` and load `purview-sourcegenerator-testing` and `purview-sourcegenerator-tunit-tests` before writing or changing these tests.

## Source generators

The `[ZodSchema]` attribute (generated into the `ZodSharp` namespace) marks a class or struct for compile-time validator generation. The generator produces a `{TypeName}Schema` static class with `Validate`/`Parse`, optional value-first composition methods (`ApplyAnd`, `ApplyOr`, `ApplyRefine`, controlled by `EnableComposition`, default `true`), and optional `IValidateOptions<T>` support.

The generator and analyzer are built with `Purview.SourceGeneratorFramework`:

- Use `CodeWriter` emission and incremental pipelines; load `.agents/skills/source-generator-codewriter-modernization/SKILL.md` before implementing, reviewing, or refactoring generator/analyzer code.
- Use `CodeWriter` emission and incremental pipelines; load `purview-sourcegenerator-codewriter` (`npx skills add purview-dev/sourcegenerator-framework`) before implementing, reviewing, or refactoring generator/analyzer code.
- Keep pipeline values immutable and value-equatable; never retain `ISymbol`, `Compilation`, `SemanticModel`, `IOperation`, `SyntaxNode`, or `Location` in pipeline models.
- Use `ForAttributeWithMetadataName` for attribute-driven discovery.
- Test incrementally, not just generated text (see the skills above).
Expand All @@ -116,7 +119,28 @@ Keep `purview-build.json`'s `PackValidation` requirements in sync with any packa

## Cross-platform fixtures

Schemas and fixtures shared between TypeScript/Zod and C#/Purview.ZodSharp live under `src/ts/`. The fixture generator (`bun run generate-fixtures`) writes JSON fixtures and a `manifest.json` that the C# tests consume; the C# cross-platform tests write output to `src/tests/cross-platform/output/`, which the vitest tests (`tests/ts/cross-platform.test.ts`) read. Keep the TS `UserSchema` and the C# `CrossPlatformUserSchema` in sync.
Schemas and fixtures shared between TypeScript/Zod and C#/Purview.ZodSharp live under `src/ts/`. The fixture generator (`bun run generate-fixtures`) writes JSON fixtures and a `manifest.json` that the C# tests consume. Keep the TS `UserSchema` and the C# `CrossPlatformUserSchema` in sync.

`src/tests/cross-platform/` is gitignored, so it starts empty on a clean checkout and in CI: **the C# producers must run before vitest**, in the same workspace. There are **two** producer/consumer pairs, in different assemblies, namespaces and classes:

| Vitest consumer | Reads | C# producer |
| --- | --- | --- |
| `tests/ts/cross-platform.test.ts` | `src/tests/cross-platform/output/` | `ZodSharp.Json/*CrossPlatformTests` (the SystemTextJson and NewtonsoftJson unit tests) |
| `tests/ts/guid-uuid-parity.test.ts` | `src/tests/cross-platform/guid-parity/` | `ZodSharp.SourceGenerators/ZodSchemaGeneratorTests/JsonSchema_GivenGeneratedGuidSchemas_WritesZodParityArtifacts` |

`just test` runs the whole suite, so it covers both producers. The `cross-platform` job in `.github/workflows/pr.yml` selects them with treenode filters instead, and therefore needs **one step per producer** — a single `a|b` filter is not honoured as a top-level alternation and silently selects a broader set while skipping the parity producer. Adding a producer means adding a step. Both vitest suites fail loudly rather than passing vacuously when their directory is empty, which is the backstop for exactly this mistake.

`src/tests/Repository.UnitTests/CrossPlatformVitestTests.cs` additionally drives the vitest suites **from the .NET test run**, as child processes through CliWrap, so `just test` alone proves both halves of the guarantee and a TypeScript-side regression fails a local run. It is a deliberate duplicate of the CI job, not a replacement — the job stays the authoritative gate because it controls producer ordering and installs bun from scratch. Three rules keep the duplication honest:

- **One test per suite**, so a failure names the `.test.ts` file, and the child process's stdout/stderr becomes the assertion message.
- **`VitestSuites_GivenEveryTestFileOnDisk_AreAllCovered`** fails when a `tests/ts/*.test.ts` file has no entry in `Suites()`, so a new suite cannot be driven by the CI job alone.
- **These tests never skip.** A skipped test proves nothing, and the parity guarantee is not optional. The runner is resolved per run: **bun** when it is on PATH, otherwise **node**. If neither is available, or the install or the run fails, the test fails and names the step.

The node fallback invokes vitest's entry point (`node_modules/vitest/vitest.mjs`, from vitest's own `package.json` `bin` field) directly, rather than `npx` or `node_modules/.bin`. Both are unreliable: the `.bin` shims are written by whichever package manager installed the tree — a bun install leaves `vitest.exe`/`vitest.bunx` and no node shim — and on Windows `npx` resolves to a `.cmd`/`.ps1` shim, which cannot be started directly as a child process. For the same reason the PATH probe only accepts a real executable image (`.exe`/`.com`), not a shim.

`node_modules/` is gitignored, so when vitest is missing the tests install it first — with `bun install --frozen-lockfile`, or `npm install` when only node is present. There is no npm lockfile (`bun.lock` is the only one), so npm resolves from `package.json`; that is a second reason bun is preferred for both the install and the run.

Because `src/tests/cross-platform/` is gitignored and the producers live in other assemblies that may still be running in parallel, these tests wait for their fixture directory to fill before skipping, rather than nesting a second `dotnet test` inside the run.

## Commit conventions

Expand All @@ -129,3 +153,44 @@ Commits must follow [Conventional Commits](https://www.conventionalcommits.org/)
- `just lint-check` (CSharpier) reports no formatting changes.
- Generated code is deterministic and reviewable; no scope leaks in `CodeWriter` output.
- Packed packages match `purview-build.json` `PackValidation` (including per-package `README.md` and `purview-logo-light.png`).

## Agent skills

Authored skills live in the repository-root `skills/` folder. **Nothing packs them into a NuGet
package** - `Purview.BuildSdk` 1.0.6 removed skill packaging outright. The skills CLI clones the *git*
repository and only discovers a root `skills/` folder, so that is the one delivery route.

`.agents/skills/` is the CLI's install location for skills from **other** repositories, and is
gitignored. This repository's own skills are never installed into it - that would leave two copies to
drift apart. Read them at their authored path, `skills/<name>/SKILL.md`.

```bash
npx skills add purview-dev/zodsharp
```

| Skill | Answers |
| --- | --- |
| `skills/purview-zodsharp-core/` | the runtime API: packages, the `Z` factory, `Validate`/`Parse`, `ValidationResult`, composition, coercion |
| `skills/purview-zodsharp-source-generator/` | `[ZodSchema]`: what it emits, its options, DataAnnotations, `OnZodValidate`, enum validation, `IValidateOptions` |
| `skills/purview-zodsharp-custom-rules/` | writing a rule, `ErrorCode`/`MessageFormat`, `IZodRule`, `[ZodRule]` attributes, scalar value objects |
| `skills/purview-zodsharp-json/` | System.Text.Json and Newtonsoft integrations, JSON Schema export and import, TypeScript Zod interop |
| `skills/purview-zodsharp-aspnetcore/` | ProblemDetails, minimal-API validation, `IZodSchemaFactory` registration, options validation, `ErrorType` |
| `skills/purview-zodsharp-diagnostics/` | what a `ZODSGEN` or `ZODSASP` diagnostic means and how to fix it |

Every skill carries the `purview-zodsharp` prefix. Skills install into one flat directory per agent,
shared with every skill a developer has installed from anywhere, so an unprefixed name collides and
the loser is whichever installed second.

To add one: create `skills/<name>/SKILL.md` with spec-legal frontmatter. There is no per-project
registration.

`src/tests/Repository.UnitTests/SkillTests.cs` validates every skill on every test run, through
`Purview.Skills.Testing`'s rule set (the Agent Skills specification: frontmatter fields, name and
description constraints, bundled links, body size). The `[InheritsTests]` attribute on that class is
load-bearing - TUnit does not walk base types during discovery, so without it the run is green and
nothing is checked. That project is single-targeted at `net10.0`, because it asserts something about
the repository rather than about the runtime's reach.

A relative link from one `SKILL.md` into another skill's folder is a rule violation: only a skill's
own folder is installed with it, so such a link works here and nowhere else. Name the other skill in
prose instead, and keep deep detail in the skill's own `references/`.
Loading
Loading