Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
136 changes: 0 additions & 136 deletions .agents/skills/value-objects/SKILL.md

This file was deleted.

43 changes: 25 additions & 18 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
},
"dotnet-inspect": {
"version": "0.25.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
}
}
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
},
"dotnet-inspect": {
"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 @@ -7,6 +7,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 All @@ -24,4 +28,4 @@
"Release": {
"Mode": "None"
}
}
}
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
with:
# The shared workflow defaults to the 10.0.x SDK, which cannot build the net11.0 target this
# repository gained (NETSDK1045). 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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
with:
# The shared workflow defaults to the 10.0.x SDK, which cannot build the net11.0 target this
# repository gained. Keep in sync with `sdk.version` in global.json (and with 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
47 changes: 43 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ a diagnostic analyzer, a code fix, tests, samples, and documentation for scalar

## Runtime project rules

- The runtime targets `net8.0;net9.0;net10.0`. Static abstract interface members (`IScalarValueObject`) require a
net7+ floor.
- The runtime targets `net8.0;net9.0;net10.0;net11.0`, selected through `PurviewTargetFrameworkSet=All` rather
than a literal list, so the set moves with `Purview.BuildSdk`. Static abstract interface members
(`IScalarValueObject`) require a net7+ floor.
- `ScalarJsonConverterFactory` uses only reflection over `ScalarAttribute` + `Create`/`Hydrate`/constructor;
keep it free of event-sourcing dependencies.
- Keep the runtime package dependency-free (System.Text.Json is in-box for the supported TFMs).
Expand Down Expand Up @@ -130,7 +131,7 @@ just pipeline-pr
```

The repository uses the shared `Purview.Build` pipeline (`purview-build.json` at the root). `just pipeline-pr`
installs the pinned `Purview.Build` tool to `.tools/purview-build` when missing.
runs `dotnet tool restore`, which installs the pinned `Purview.Build` from `.config/dotnet-tools.json`.

Local CI-equivalent validation:

Expand Down Expand Up @@ -165,4 +166,42 @@ Before handing work back:
- Add or update focused tests for code changes.
- Update all affected docs, samples, analyzer metadata, and release notes when applicable.
- Run appropriate build, test, formatting, and pack checks in proportion to risk.
- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and remaining risk.
- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and remaining risk.
## Agent skills

Authored skills live in the repository-root `skills/` folder. Nothing packs them: `Purview.BuildSdk`
delivers skills through the skills CLI, which clones the *git* repository and only discovers a root
`skills/` folder.

`.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`.

Install these skills into your agent's directories with:

```bash
npx skills add purview-dev/value-objects
```

| Skill | Answers |
| -- | -- |
| `skills/purview-value-objects/` | modelling a value object: scalar, complex, contextual, `Create`/`Hydrate`, normalization, typed identifiers |
| `skills/purview-value-objects-efcore/` | persisting one with EF Core: converters, complex-type vs JSON, keys, generated key values, migrations |
| `skills/purview-value-objects-zodsharp/` | validating one with ZodSharp: `[ZodSchema]`, `ZodSchemaMode`, `OnZodValidate`, custom rules, Problem Details |
| `skills/purview-value-objects-diagnostics/` | what a `VO1xxx` means and how to fix it |

Every skill carries the `purview-value-objects` 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.

`src/tests/Repository.UnitTests/SkillTests.cs` validates them 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 pins `net10.0` rather than taking the repository's `All` set,
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.

52 changes: 46 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ All notable changes to this repository are recorded here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). `package.json` is the authoritative version.

Released versions correspond to `v<version>` GitHub releases. Entries below the `Unreleased` heading have not
been published to NuGet. No stable release has been cut yet; the published line is `1.0.0-prerelease.N`.
Released versions correspond to `v<version>` GitHub releases. `package.json` is the authoritative version; the
newest heading below is the version in development. Earlier releases are listed under
[releases](https://github.com/purview-dev/value-objects/releases).

## Unreleased
## 1.1.0

### Added

- Improved ZodSharp integration, including updated generation of Zod rule attributes.
- MIT `LICENSE.md`.
- [Diagnostics](docs/Diagnostics.md): a consolidated reference for all 17 `VO1xxx` rules. `VO1002`–`VO1008`
- [Diagnostics](docs/Diagnostics.md): a consolidated reference for all 19 `VO1xxx` rules. `VO1002`–`VO1008`
previously appeared only in the analyzer release table, and the Entity Framework and ZodSharp rules were split
across two guides.
- **Automatic scalar declaration.** `[Scalar<TValue>]` and `[Scalar(typeof(TValue))]` let the generator
Expand All @@ -26,11 +27,41 @@ been published to NuGet. No stable release has been cut yet; the published line
- **Nullable scalars round-trip JSON `null`.** The generated converter and `ScalarJsonConverterFactory`
accept `null` for a nullable scalar (a `T?` reference or value type) and still reject it for a non-nullable
reference scalar.
- **ZodSharp integration for the automatic forms.** With `Purview.ZodSharp 2.0.2`, `[Scalar<T>]` /
- **ZodSharp integration for the automatic forms.** With `Purview.ZodSharp 2.0.2` or later, `[Scalar<T>]` /
`[Scalar(typeof(T))]` combined with `[ZodSchema]` generate a `{Type}Schema` from the attribute (the
underlying type and property name) and validate through `Create`, even though the property is not visible
to the ZodSharp generator. ZodSharp 2.0.2 also ships `[NullOrNonWhiteSpace]` for the nullable
"null or non-whitespace" rule.
- **Scalar formatting.** The generated value object now mirrors the underlying value's formatting overloads
per type. When the wrapped type implements `System.IFormattable` (for example `decimal`, `Guid`,
`DateTime`, or an enum), the value object implements `IFormattable` and forwards
`ToString(string? format, IFormatProvider? formatProvider)`. When the wrapped type declares a format-only
`ToString(string? format)` (for example `Guid`, `DateTime`, or an enum), that overload is forwarded too.
The value object exposes the same formatting options as the property it wraps, so
`amount.ToString("N2", CultureInfo.InvariantCulture)`, `id.ToString("N")`, and
`string.Format("{0:N2}", amount)` behave as they would on the underlying value. A scalar whose underlying
type declares neither overload (`string`, `bool`) is unchanged, a null underlying value formats as the
empty string, and an author-declared overload is left untouched.
- **Scalar interface mirroring.** The generated value object now implements the same standard interfaces as
the type it wraps, so it behaves like that type in equality, formatting, and parsing contexts:
- `IEquatable<TValue>` when the underlying type implements it, and `IEquatable<TSelf>` for non-record
classes (previously only non-record structs and records implemented it).
- `ISpanFormattable` / `IUtf8SpanFormattable` when the underlying type implements them, forwarding
`TryFormat` (a null underlying value writes nothing and reports success).
- `IParsable<TSelf>` / `ISpanParsable<TSelf>` / `IUtf8SpanParsable<TSelf>` when the underlying type
implements them; `Parse` validates through `Create`, and `TryParse` returns `false` (through `TryCreate`)
for input that does not parse or fails domain validation.

Detection is per type and per target framework, so a scalar only gets the interfaces its underlying type
actually implements; an interface is skipped when the author already declares one of its members. Numeric
and arithmetic interfaces, `IConvertible`, and collection interfaces are intentionally not mirrored.
- **Complex value objects implement `IEquatable<TSelf>`.** A `[ValueObject]` class now implements
`IEquatable<TSelf>` (previously only non-record structs and records did), so `EqualityComparer<T>` and
generic equality treat it by value. The generated `Equals(TSelf)` accepts a nullable reference, as the
interface requires.
- **NuGet package validation.** The runtime package enables `EnablePackageValidation` against the previous
stable release (`1.0.1`), so an accidental breaking change to the public API fails `dotnet pack` rather
than shipping.

### Fixed

Expand Down Expand Up @@ -99,6 +130,15 @@ been published to NuGet. No stable release has been cut yet; the published line
`CompareTo`/`Equals` overload was ambiguous for a nullable value type, and `GetHashCode`/`ToString`
produced nullable warnings. The generated code now strips the annotation where it is not representable and
handles null in those members.
- **`GenerateComparable` now does what its documentation says.** The option claimed to control
`IComparable<T>`, but the value object always implements `IComparable<TSelf>`/`IComparable` (and a scalar
always exposes `CompareTo(TValue)`) because the runtime contract requires it. `GenerateComparable = false`
now drops the extra `IComparable<TValue>` interface as well as the comparison operators, and the
documentation on `ScalarOptionsAttribute`, `ValueObjectAttribute`, and `ValueObjectDefaultsAttribute`
describes the actual behaviour.
- **An author-declared `Equals` no longer conflicts with the mirrored `IEquatable` interface.** When a scalar
declares `Equals(TValue)`, or a value object declares `Equals(TSelf)`, with a non-nullable reference
parameter, the generated `IEquatable<T>` is left off instead of emitting `CS8767` in the consumer's build.

### Changed — trimming and Native AOT

Expand Down Expand Up @@ -148,7 +188,7 @@ been published to NuGet. No stable release has been cut yet; the published line
value, `ScalarValueObjectEmitter.EmitConstructor` **emits a private one**, which is the documented and
tested behaviour. The rule as written contradicted the generator. The id now sits with `VO1011`, `VO1012`,
`VO1014` and `VO1020` as retired and never reused, leaving 17 live rules.
- **`VO1022` ships in a new `## Release 1.0.1` block** in `AnalyzerReleases.Shipped.md`, for the automatic
- **`VO1022` ships in a new `## Release 1.1.0` block** in `AnalyzerReleases.Shipped.md`, for the automatic
scalar forms that declare the underlying property themselves.

### Versioning and package lineage
Expand Down
Loading
Loading