Skip to content
Open
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
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ so moving a project cannot silently retarget a test.
- `test/CodoMetis.ValueRanges.EFCore.PostgreSQL.IntegrationTests/` — Live PostgreSQL via Testcontainers (needs Docker; Inconclusive without, but hard failure under CI=true). The shared container runs `VALUERANGES_POSTGRES_IMAGE`, default `postgres:16-alpine` (the 10.x floor); CI runs both integration projects on 16 and 18, and a server below 16 fails every test (`PostgresServer.cs`). Covers BCL and NodaTime types. Authority on PostgreSQL semantics — run when changing translations or range algebra. `CapturedCollectionProbeTests` sends every discovered range and set type as a captured collection (`Contains`, `Any`, `Count`); it links `RangeProbes` and `SetProbes` from the conventions project rather than copying them. `ComplexIndexesCrossTests` holds this package's columns under `EFCore.ComplexIndexes.PostgreSQL`'s temporal and exclusion constraints and GiST indexes on PostgreSQL 16 and 18 (containers of its own) through a test-only reference; neither shipping package references the other. Off on the 11 line (the reference and the file are conditioned on `net10.0`) until ComplexIndexes ships its own 11 preview, CaffeinatedCoder/EFCore.ComplexIndexes#38; its #42 turns them back on
- `test/CodoMetis.ValueRanges.EFCore.PostgreSQL.InfinityDisabled.IntegrationTests/` — The same live layer for configuration that is per process: its runtimeconfig sets `Npgsql.DisableDateTimeInfinityConversions`, which Npgsql and the plugin each read once, so it cannot be flipped inside the other project's run. Holds the temporal store rule at the extremes with the conversion off. Own container; CI's integration job runs every `test/*IntegrationTests/`
- `test/CodoMetis.ValueRanges.AspNetCore.Tests/` — Host-level behaviour with no ValueRanges registration: configuration binding (`TypeConverter`), MVC and minimal-API binding and JSON bodies via `TestServer`, and the documented OpenAPI schema transformer (`ValueRangesSchemaTransformer.cs`), which `docs/serialization.md` must carry verbatim — edit the test copy, then the doc. `HostCultureProbeTests` binds every discovered type (core and NodaTime) through both hosts under four request cultures; it links `RangeProbes`, `SetProbes`, `WrapperElements` and `Reflect` from the conventions project rather than copying them
- `test/CodoMetis.ValueRanges.Conventions.Tests/` — Repo-level conventions: changelog consistency and form, packaging metadata, release wiring, the SECURITY.md supported-versions table, value set contract compliance, EF mapping parity, the default JSON contract. Everything is discovered (projects by globbing `src/`, types by reflection), so adding a package or a type needs no edit here
- `test/CodoMetis.ValueRanges.Conventions.Tests/` — Repo-level conventions: changelog consistency and form, packaging metadata (each package's nuspec, generated by NuGet's own pack task, carries its version's changelog section as release notes), release wiring, the SECURITY.md supported-versions table, value set contract compliance, EF mapping parity, the default JSON contract. Everything is discovered (projects by globbing `src/`, types by reflection), so adding a package or a type needs no edit here
- `test/oracles/` — The bug-finding system (`docs/oracles.md`): `claims.json` quotes every promise the consumer-facing docs make and names the test that holds it (empty `heldBy` is the worklist); `oracles.json` registers the sweep classes; `seeds/*.patch` plant one defect each in `src/` that the named oracles were shown to catch; `surface-waivers.json` names the public members no claim covers, with the reason; `candidates.json` holds disagreements found but not fixed, keyed to issues; `compiled-out.json` records a test class a project compiles out on this line (a `heldBy` must be compiled, so its claims count as unheld) with the issue that compiles it back; `postgres-surface.json` holds every PostgreSQL range, multirange and array operator and function (one entry per row of `postgres-surface.catalog.txt`, the raw `pg_operator`/`pg_proc` listing) with the member that mirrors it, the translation a test asserts, or a waiver or issue; `CloneSweep.cs` is the clone-divergence sweep over the hand-copied type families (sets, arities, ranges, EF registrations), and `clone-waivers.json` explains every divergence it finds between siblings that is right as it is. `Oracle*Tests` and `CloneDivergenceTests` in the conventions project hold all of it consistent in CI without building; `dotnet run tools/oracles.cs seeds` re-proves locally that every seed is caught, `… status` prints the worklist, `… pg-surface` regenerates the listing from a container, `… clones` prints the sibling divergences with their waiver status
- `tools/oracles.cs` — The local prover and status report for `test/oracles/`; file-based app, never run in CI
- `test/consumer-smoke-test.sh` — The only check of *delivery* rather than code: packs the four packages, restores them into throwaway projects created outside the repository (inside it, `Directory.Build.props` would apply), compiles and runs real code against them, and asserts on printed output and translated SQL — never on the exit code alone. Four consumers: core only; a Native AOT publish of core + NodaTime, run as a native binary (extra linker paths via `SMOKE_AOT_LINKER_ARGS`, e.g. for Homebrew's dotnet); the NodaTime EF satellite, which pulls in all four through the nuspec chain; and the EF package beside `EFCore.ComplexIndexes.PostgreSQL` from nuget.org, asserting its exclusion and temporal constraints over this package's columns in the create script (skipped on the 11 line until CaffeinatedCoder/EFCore.ComplexIndexes#38, like `ComplexIndexesCrossTests`). `NUGET_PACKAGES` is redirected to a private folder because NuGet resolves id+version from the global cache before any source, so a locally built package is otherwise shadowed by whatever build of that version was restored before; the generated `nuget.config` uses package source mapping so this repo's ids come from the local feed only and everything else from nuget.org. Runs on every PR in both modes (own feed; the pre-built `dist` the release workflow passes). Everything is discovered (projects by globbing `src/`, types by reflection), so adding a package or a type needs no edit here
- `test/consumer-smoke-test.sh` — The only check of *delivery* rather than code: packs the four packages, restores them into throwaway projects created outside the repository (inside it, `Directory.Build.props` would apply), compiles and runs real code against them, and asserts on printed output and translated SQL — never on the exit code alone. Before the consumers it reads each `.nupkg`'s nuspec: the release notes open with the version's changelog section in plain text and end with the changelog at the version's tag. Four consumers: core only; a Native AOT publish of core + NodaTime, run as a native binary (extra linker paths via `SMOKE_AOT_LINKER_ARGS`, e.g. for Homebrew's dotnet); the NodaTime EF satellite, which pulls in all four through the nuspec chain; and the EF package beside `EFCore.ComplexIndexes.PostgreSQL` from nuget.org, asserting its exclusion and temporal constraints over this package's columns in the create script (skipped on the 11 line until CaffeinatedCoder/EFCore.ComplexIndexes#38, like `ComplexIndexesCrossTests`). `NUGET_PACKAGES` is redirected to a private folder because NuGet resolves id+version from the global cache before any source, so a locally built package is otherwise shadowed by whatever build of that version was restored before; the generated `nuget.config` uses package source mapping so this repo's ids come from the local feed only and everything else from nuget.org. Runs on every PR in both modes (own feed; the pre-built `dist` the release workflow passes). Everything is discovered (projects by globbing `src/`, types by reflection), so adding a package or a type needs no edit here
- `docs/` — Agent docs (read relevant doc before starting work)

## Commands
Expand All @@ -46,6 +46,14 @@ surface of the shipping projects — the shipped `.xml` is what IntelliSense sho
internal-API usage (EF1001) is acknowledged with a file-level pragma next to the reason, never
repo-wide.

Pack writes each package's release notes (nuget.org's Release Notes tab, Visual Studio's Updates
tab) from the `## [$(Version)]` section of its own `src/<package>/CHANGELOG.md`, through the target
in `Directory.Build.targets`. They are plain text, because nuget.org shows release notes as typed
and links only bare URLs: a link becomes `text (url)`, and backticks, bold and `###` are dropped.
Pack fails, naming the file, when the section is missing, empty, doubled or over nuget.org's
35,000 characters, so a version bump needs its section in all four package changelogs before
`dotnet pack` runs. Never set `PackageReleaseNotes` anywhere else.

## Workflow
1. Read `docs/architecture.md` before modifying range types or interfaces
2. Explore the codebase — range operations are per-shape (Finite, UnboundedStart, etc.)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The first preview of the 11 line: the same public API as 10.0.0, on .NET 11. Not

- EF Core: an `ExecuteUpdate` that sets a nullable value set inside a JSON column to a value that may be null is refused for the JSON array alone; Npgsql 11 writes the null outcome as JSON null, so the message no longer names a whole-document NULL ([JSON columns](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/efcore.md#put-a-range-or-set-inside-a-json-column)).
- EF Core: an empty captured list of ranges or sets reads no rows through EF Core 11's own `SELECT … WHERE FALSE`; the SQL differs from 10.x, the answers do not ([Captured lists](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/efcore.md#match-a-column-against-a-list-of-sets)).
- Packaging: a package's release notes on nuget.org and in Visual Studio are its version's section of its changelog, in plain text, ending with a link to the changelog at the version's tag, where they were a link to the latest changelog ([Changelog](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/CHANGELOG.md)).

### Fixed

Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ fails outright.

A version's section of the root `CHANGELOG.md` is the GitHub release's notes, word for word, so it
is written for someone deciding what to change before they upgrade. The package changelogs use the
same form, with the entries that affect that package.
same form, with the entries that affect that package, and a package's section is its release notes
on nuget.org and in Visual Studio: pack writes it into the nuspec in plain text, because nuget.org
shows no Markdown, and fails when the section is missing or empty (`Directory.Build.targets`).

- **The heading** is `## [x.y.z] — yyyy-mm-dd`. Write entries under the version being prepared,
never under `Unreleased`. The date is the tag's: set it in all five changelogs in the commit that
Expand All @@ -208,7 +210,7 @@ same form, with the entries that affect that package.
where it helps a reader skim (`JSON:`, `Literals:`, `EF Core:`, `NodaTime:`). In `Added`, lead
with the new member in bold.
- **Keep each section in area order:** `Ranges`, `Value sets`, `Literals`, `JSON`, `ASP.NET Core`,
`EF Core`, `NodaTime`, `Docs`, so a reader finds an area's lines together. In `Added`, the core
`EF Core`, `NodaTime`, `Docs`, `Packaging`, so a reader finds an area's lines together. In `Added`, the core
members come first, then `EF Core:`, then `Docs:`. Use no headings below the three sections.
- **End with a link** to the guide that explains it. Links are absolute
(`https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/…`), because the
Expand Down
6 changes: 2 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
<PackageProjectUrl>https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges</PackageProjectUrl>
<RepositoryUrl>https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- Each package's own changelog, which is short enough to read on the NuGet page's link.
The project directory is named after the project, so this resolves for every package
under src/; PackagingConventionTests checks that it does. -->
<PackageReleaseNotes>https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/src/$(MSBuildProjectName)/CHANGELOG.md</PackageReleaseNotes>
<!-- PackageReleaseNotes is set at pack time from the version's section of the package's own
CHANGELOG.md, by the target in Directory.Build.targets. Nothing sets it here. -->
</PropertyGroup>

<!--
Expand Down
118 changes: 118 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<Project>
<!--
The package's release notes are its version's section of its own CHANGELOG.md (#346).

nuget.org's Release Notes tab and Visual Studio's Updates tab show PackageReleaseNotes. A
link there sent a reader of any version to today's file. The section under
`## [$(Version)]`, up to the next `##`, is exactly the notes for the version they are
looking at, so pack writes that, and one footer line links the whole changelog at the
version's tag. The tag is immutable and is pushed before anything is published
(release.yml), so the link resolves for every version on nuget.org, on either line.

nuget.org shows release notes as plain text, not Markdown (measured on a package page, see
#346): it HTML-encodes them, turns each newline into <br />, and links only bare http(s)
URLs, stopping before a closing parenthesis. So a link `[text](url)` is written
`text (url)`, which still links, and the inline-code backticks, the bold markers and the
`### ` of a section heading are dropped, so a line reads as a sentence.

Pack fails, naming the file, when the section is missing or empty, or when the notes are
longer than the 35,000 characters nuget.org accepts. A package whose section does not
exist would otherwise ship with no notes, or with some other version's.
PackagingConventionTests packs every package's nuspec and compares its releaseNotes with
the section, transformed independently of this task, and the consumer smoke test checks
the packed .nupkg.
-->
<PropertyGroup>
<ReleaseNotesChangelog>$(MSBuildProjectDirectory)/CHANGELOG.md</ReleaseNotesChangelog>
</PropertyGroup>

<!-- Runs before the nuspec is generated, after GenerateNuspec's own dependencies, on every pack;
the changelog is a pack input, so an edit to it alone repacks. -->
<Target Name="SetPackageReleaseNotesFromChangelog"
BeforeTargets="GenerateNuspec"
Condition="'$(IsPackable)' == 'true'">
<PropertyGroup>
<_ReleaseNotesChangelogPath>$([System.IO.Path]::GetRelativePath('$(MSBuildThisFileDirectory)', '$(ReleaseNotesChangelog)').Replace('\', '/'))</_ReleaseNotesChangelogPath>
</PropertyGroup>

<ChangelogReleaseNotes
Changelog="$(ReleaseNotesChangelog)"
Version="$(Version)"
FullChangelogUrl="$(RepositoryUrl)/blob/v$(Version)/$(_ReleaseNotesChangelogPath)">
<Output TaskParameter="ReleaseNotes" PropertyName="PackageReleaseNotes" />
</ChangelogReleaseNotes>

<ItemGroup>
<NuGetPackInput Include="$(ReleaseNotesChangelog)" />
</ItemGroup>
</Target>

<UsingTask TaskName="ChangelogReleaseNotes"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<Changelog ParameterType="System.String" Required="true" />
<Version ParameterType="System.String" Required="true" />
<FullChangelogUrl ParameterType="System.String" Required="true" />
<ReleaseNotes ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Linq" />
<Using Namespace="System.Text.RegularExpressions" />
<Code Type="Fragment" Language="cs"><![CDATA[
const int NuGetLimit = 35000;

if (!File.Exists(Changelog))
{
Log.LogError("{0} does not exist, so the package has no release notes. Every package keeps its own CHANGELOG.md.", Changelog);
return false;
}

var lines = File.ReadAllText(Changelog).Replace("\r\n", "\n").Split('\n');
var heading = new Regex(@"^## \[" + Regex.Escape(Version) + @"\](\s|$)");
var starts = Enumerable.Range(0, lines.Length).Where(index => heading.IsMatch(lines[index])).ToList();

if (starts.Count != 1)
{
Log.LogError(
starts.Count == 0
? "{0} has no '## [{1}]' section, so the package has no release notes for {1}. Add the section, or \"No change in this package.\" under it."
: "{0} has more than one '## [{1}]' section; the release notes would be ambiguous.",
Changelog, Version);
return false;
}

var section = lines.Skip(starts[0] + 1)
.TakeWhile(line => !line.StartsWith("## ", StringComparison.Ordinal))
.Select(line => line.TrimEnd())
.SkipWhile(line => line.Length == 0)
.Reverse().SkipWhile(line => line.Length == 0).Reverse()
.ToList();

if (section.Count == 0)
{
Log.LogError("{0}'s '## [{1}]' section is empty, so the package would ship with no release notes. Say \"No change in this package.\" if that is the case.", Changelog, Version);
return false;
}

var link = new Regex(@"\[(?<text>[^\[\]]+)\]\((?<url>https?://[^\s()]+)\)");

ReleaseNotes = string.Join("\n", section.Select(line =>
{
if (line.StartsWith("### ", StringComparison.Ordinal)) line = line.Substring(4);
line = link.Replace(line, "${text} (${url})");
return line.Replace("`", "").Replace("**", "");
}))
+ "\n\nFull changelog: " + FullChangelogUrl;

if (ReleaseNotes.Length > NuGetLimit)
{
Log.LogError("The release notes from {0}'s '## [{1}]' section are {2} characters; nuget.org accepts at most {3}.", Changelog, Version, ReleaseNotes.Length, NuGetLimit);
return false;
}
]]></Code>
</Task>
</UsingTask>
</Project>
Loading
Loading