Skip to content

docs: split the README and state what the package adds over the alternatives - #19

Merged
CaffeinatedCoder merged 3 commits into
mainfrom
docs/split-readme
Aug 16, 2026
Merged

CaffeinatedCoder merged 3 commits into
mainfrom
docs/split-readme

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Why

The README had grown to 1227 lines / 86 KB. Six What's new in vX sections and three migration
guides sat between the pitch and the API docs, so the reference material a reader actually returns
to started around line 250 and ran to the end. Separately, the READMEs explained what the library
does and never what you would otherwise use — so the first question a reader has, why not
NpgsqlRange<T>, which ships with the driver
, went unanswered on both the repo page and the NuGet
page.

Docs only. No source changes, no behavioural changes, no public API surface touched.

What changed

1. Split the README into a landing page and five reference pages (2256c3f)

Page Lines Contents
docs/ranges.md 445 Construction, pattern matching, query and set algebra, RangeSet, interface hierarchy
docs/efcore.md 228 Mapping, LINQ-to-SQL translation, timerange, YearMonthRange storage, what runs where
docs/serialization.md 189 Literals, parsing, ISpanParsable, System.Text.Json
docs/value-sets.md 114 Canonical scalar sets over native arrays
docs/migration.md 52 The v1.x / v2.x / v3.x source changes

README is now 203 lines. The What's new sections are dropped rather than moved — CHANGELOG.md
already carries all eight of those versions in full, and duplicating release notes in the README is
what let them accumulate.

2. Added positioning to both READMEs (31c7d8e) — ## Why this exists in the root README,
## Why not NpgsqlRange<T>? on the NuGet page. Built from a survey of the current alternatives
rather than a claim of uniqueness, so a reader can check each line.

3. Sharpened the comparison after review (ac589dd) — discrete domains, FRange's partial
accessors, and value-set immutability.

The comparison, and what it deliberately does not claim

Verified against the libraries' own source and docs, not their package descriptions:

  • NpgsqlRange<T> is declared in NpgsqlTypes, in Npgsql.dll — a domain model using it
    references the database driver. The struct carries no algebra at all; Contains, Overlaps,
    Union and the rest are EF Core extension methods documented "only intended for use via SQL
    translation as part of an EF Core LINQ query"
    , and throw client-side.
  • NodaTime Interval/DateInterval are real domain types the Npgsql plugin maps and
    translates — but two date/time shapes only, and DateInterval is always closed and bounded.
  • FRange keeps unboundedness a runtime fact: its C# surface exposes LowerBoundValue over an
    F# DU whose Unbounded case is failwith "No bound" (FRange/CSharp.fs).
  • Neither FRange nor Open.Range has a discrete domain — no step, no canonicalization — so
    [1,10) and [1,9] stay different values and integer or date adjacency cannot be decided.
  • T[]/List<T> are mutable references and sequence-equal, so the domain hands out its own
    invariant on load.

Two things are stated explicitly against interest, because the sections are meant to be
falsifiable:

  • Npgsql already translates the full range operator set. SQL translation is not the
    differentiator and the README says so; what is missing elsewhere is a domain type to hang it on.
  • Open.Range is not accused of accepting invalid ranges. Its constructor calls
    Range.AssertIsValid, and Boundary<T>.CanRangeWith rejects equal-valued exclusive bounds, so
    even the degenerate (5,5) throws.

The survey is dated (August 2026) and invites correction by issue.

Drive-by fix

The package README — and therefore the NuGet page — claimed
Int32Range.Parse("[1,10)", null).ToString() returns [1,10). It returns [1,9]; discrete ranges
canonicalize, per
RangeParseFormatTests.Parse_DiscreteRange_HalfOpenInput_CanonicalizesToClosed.

Verification

Content moved by line-range extraction rather than retyped, then checked mechanically:

  • 787 of 789 non-blank lines outside the deleted What's new block are byte-identical in their
    new home. The remainder are the two headings promoted to page titles and four lines reworded
    because the split broke their above/below references.
  • A link checker over all 15 markdown files confirms every relative link and anchor resolves,
    including the ~20 cross-references rewritten here.
  • The root README is not packed into any package (all four ship their own), and
    PackagingConventionTests asserts only on src/*/README.md.
  • Build clean (0 warnings, warnings-as-errors). 1117 tests pass, including the live-PostgreSQL
    Testcontainers suite.

For the reviewer

  • src/CodoMetis.ValueRanges/README.md is packed, so this PR changes what renders on the NuGet
    page. consumer-smoke-test.sh will pack it in both modes — that difference from main is
    intended, not a regression.
  • Worth a second opinion on the comparison table specifically: it names third-party libraries, and
    every row should stay defensible.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits August 16, 2026 22:36
…ages

The README had grown to 1227 lines: six "What's new in vX" sections and three
migration guides sat between the pitch and the API docs, so the reference
material a reader returns to started around line 250 and ran to the end. The
problem was organisation, not volume — nothing here is new prose.

Sections were extracted by line range rather than retyped, so 787 of the 789
non-blank lines outside the deleted "What's new" block are byte-identical in
their new home:

  docs/ranges.md         construction, pattern matching, query and set algebra,
                         RangeSet, the interface hierarchy
  docs/efcore.md         mapping, LINQ-to-SQL translation, timerange,
                         YearMonthRange storage, what runs where
  docs/serialization.md  literals, parsing, ISpanParsable, System.Text.Json
  docs/value-sets.md     canonical scalar sets over native arrays
  docs/migration.md      the v1.x / v2.x / v3.x source changes

The "What's new" sections are dropped rather than moved: CHANGELOG.md already
carries all eight of those versions in full, and duplicating release notes in
the README is what let them accumulate. The README keeps the framing, the type
matrices, install, a new quick start, a documentation index, and the
verified-against-PostgreSQL section.

Four directional references ("documented below", "the rule documented above")
became wrong once their target moved to another page and are now links. The
subsection headings in value-sets.md and efcore.md move from ### to ## because
their ## parent became the page #; GitHub slugs are level-independent, so no
anchor changed.

Verified: a link checker over all 15 markdown files resolves every relative
link and anchor, including the ~20 cross-references rewritten here. The root
README is not packed into any package (all four ship their own), and
PackagingConventionTests asserts only on src/*/README.md. Build clean, 1117
tests pass including the live-PostgreSQL suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The READMEs explained what the library does and never what you would otherwise
use, so the first question a reader has — why not NpgsqlRange<T>, which ships
with the driver — went unanswered.

Adds "Why this exists" to the root README and "Why not NpgsqlRange<T>?" to the
package README, both built from a survey of the current alternatives rather
than a claim of uniqueness, so a reader can check them:

  NpgsqlRange<T>          declared in NpgsqlTypes, in Npgsql.dll, so a domain
                          model using it references the driver; the struct has
                          no algebra, and the EF extension methods carrying it
                          are documented "only intended for use via SQL
                          translation as part of an EF Core LINQ query"
  NodaTime Interval /     real domain types the Npgsql plugin maps, but two
  DateInterval            date/time shapes only, and DateInterval is always
                          closed and bounded — never half-open, unbounded, empty
  FRange, Open.Range      in-memory algebra, no persistence or SQL translation
  T[] / List<T>           a list, not a set: order and multiplicity are part of
                          the value

Deliberately not claimed: that this is the only or first such library. The
sections state what each alternative does not cover and make the novelty claim
about the combination, which is checkable and ages honestly. The survey is
dated and invites correction by issue.

One correction worth noting separately: Npgsql already translates the full
range operator set, so SQL translation is not the differentiator and the text
says so. What is missing elsewhere is a domain type to hang it on, and any
equivalent at all on the value set side.

Also fixes a wrong comment on the package README — and so the NuGet page —
which claimed Int32Range.Parse("[1,10)") round-trips to "[1,10)". Discrete
ranges canonicalize to "[1,9]", per
RangeParseFormatTests.Parse_DiscreteRange_HalfOpenInput_CanonicalizesToClosed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mmutability

Three corrections to the positioning sections, two of them verified against the
libraries' source rather than their package descriptions.

FRange keeps unboundedness a runtime fact. Its C# surface exposes
LowerBoundValue/UpperBoundValue over an F# DU whose Unbounded case hits
`failwith "No bound"` (FRange/CSharp.fs), paired with a HasLowerBound the
caller is expected to remember. That is the same question the union design
here refuses to let you ask, so the comparison now names it.

Neither FRange nor Open.Range has a discrete domain — no step, no
canonicalization — so [1,10) and [1,9] stay different values and integer or
date adjacency cannot be decided at all. That is a bigger gap than "no
persistence" and was missing.

Value sets: immutability was the omission. T[] and List<T> are mutable
references, so a domain hands out its own invariant on load and cannot defend
it afterwards. Both READMEs now say the sets are immutable end to end and
canonical on every construction path.

Deliberately NOT claimed: that Open.Range accepts invalid ranges. Its
constructor calls Range.AssertIsValid, and Boundary<T>.CanRangeWith rejects
equal-valued exclusive bounds, so even the degenerate (5,5) throws. The only
way past it is default(Range<Boundary<T>>), which bypasses the constructor as
any validating struct does — too language-general to hang a comparison on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder merged commit 0236c1f into main Aug 16, 2026
8 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the docs/split-readme branch August 16, 2026 22:03
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