Skip to content

feat(justdummies): add JD014-JD015 and the constraint-analysis helpers - #371

Merged
Reefact merged 1 commit into
mainfrom
claude/justdummies-analyzers-analysis-z9q1xd
Jul 29, 2026
Merged

feat(justdummies): add JD014-JD015 and the constraint-analysis helpers#371
Reefact merged 1 commit into
mainfrom
claude/justdummies-analyzers-analysis-z9q1xd

Conversation

@Reefact

@Reefact Reefact commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Opens the JustDummies.Constraints category and lands the shared helpers the remaining constraint rules need. These rules front-load, to build time, the subset of the library's run-time constraint checks that is decidable from compile-time constants — the run-time checks stay, since they cover every argument these cannot see.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

  • ConstantFacts — reads statically-known values, deliberately wider than IOperation.ConstantValue: a TimeSpan is never a C# constant, yet TimeSpan.Zero and TimeSpan.FromSeconds(1) are as statically known as any literal, and the granularity constraints that take one are exactly the ones worth checking.
  • AnyChainFacts — walks a fluent chain back to its factory and yields the constraint calls in order. Syntactic, without dataflow: a rule claiming a chain is unsatisfiable must see every constraint it carries, so a chain split across statements is not followed rather than half-understood.
  • JD014 RejectedConstantArgument (Warning) — a constant argument the generator's own guard refuses, so the call throws every time it runs. One rule over one table mirroring SizeGuard and the per-generator guards: Between(10, 5), MultipleOf(0), WithLength(-1), WithScale(29), WithChars(""), OneOf(), WithGranularity(TimeSpan.Zero), and the rest.
  • JD015 StringConstraintsAdmitNoValue (Warning) — an AnyString chain whose constant constraints admit no value. This is the case ADR-0035 names by hand as the one an analyzer should carry and the type system cannot: Numeric().StartingWith("ORD-") conflicts while Numeric().StartingWith("123") does not, from identical call sites and identical static types.
  • 24 new analyzer tests (125 total), 4 documentation pages (EN + FR), both index tables with a new Constraints section, release-tracking rows.

Dogfooding corrected JD015 twice, in opposite directions

Both corrections are now pinned by tests that cite the sites which produced them.

1. Casing is not a character set. The first version modelled UpperCase() as the pool A-Z0-9, and flagged Any.String().UpperCase().StartingWith("ORD-") — a chain JustDummies.UnitTests/AnyStringTests.cs asserts is legal, with the display name "fragments keep their own characters". Reading StringSpec.ValidateFragmentCharacters confirms it: casing checks FirstAgainstCasing, which looks only at letters. So the - passes, while UpperCase().StartingWith("abc") correctly throws on the lowercase a.

2. The length budget is real, but a value set switches it off. I had also suspected the budget was unsound (overlapping Containing fragments can share characters). A probe against the built library says otherwise — WithMaxLength(6).StartingWith("SKU-").EndingWith("-EUR") does throw. What actually explains the second hit is OneOf: with a terminal value set declared, the fragments are matched against the pooled values rather than laid out side by side, so OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba") is legal even though the fragments sum to 4.

Both facts were verified against the built library before being encoded, not inferred from reading the guards. After the corrections, JD014 and JD015 produce zero hits across JustDummies.UnitTests, JustDummies.PropertyTests, JustDummies.Xunit.UnitTests, FirstClassErrors.Testing.UnitTests and FirstClassErrors.UnitTests.

Testing

  • dotnet build FirstClassErrors.sln — 0 warnings, 0 errors
  • dotnet test FirstClassErrors.sln — 2 042 tests, 0 failures across all 13 test projects
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests, 132) — the rules added here live in JustDummies.Analyzers.UnitTests (125)

Mutation testing was not run locally.

Documentation

  • Public API / error documentation updated — no public API change
  • README / doc/ updated — JD014/JD015 pages and a new Constraints section in the rule index
  • French translation updated — JD014.fr.md, JD015.fr.md and the French index
  • No documentation change required

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-____
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

Worth a second opinion, though: this category's premise — that front-loading the constant subset of the run-time checks is desirable, and that the run-time checks stay — is arguably a lasting decision. My study drafted it as one. I have not written the ADR, because ADR-0035 already states the rule these follow ("an analyzer is the right tool for a value-dependent smell the types cannot catch") and JD015 is the very example it names, so this looks like application rather than decision. Tell me if you read it the other way and I will draft the successor.

Remaining

This is the first of the constraint waves. Fifteen catalogued rules remain: the scalar-chain emptiness test, collection counts and cardinality, the enum universe, no-effect constraints, duplicate pool values, the empty relative URI, the unused Combine operand, Distinct over reference equality, and the reproducibility long tail. Two carry their own decision and I will surface them rather than decide alone — the unsupported-regex rule needs the grammar shared with the library (a linked source file), and the production-code rule needs the IsTestProject MSBuild oracle, with FirstClassErrors.Testing as a live counterexample.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq


Generated by Claude Code

Opens the JustDummies.Constraints category, which front-loads to build
time the subset of the library's run-time constraint checks that is
decidable from compile-time constants. The run-time checks stay: they
cover every argument these cannot see.

Three helpers carry it. ConstantFacts reads statically-known values and
is deliberately wider than IOperation.ConstantValue, because a TimeSpan
is never a C# constant yet TimeSpan.Zero and TimeSpan.FromSeconds(1) are
as statically known as any literal — and the granularity constraints that
take one are exactly the ones worth checking. AnyChainFacts walks a
fluent chain back to its factory and yields the constraint calls in
order, syntactically and without dataflow: a rule claiming a chain is
unsatisfiable must see every constraint it carries, so a chain split
across statements is not followed rather than half-understood.

JD014 reports a constant argument the generator's own guard refuses, as
one rule over one table mirroring SizeGuard and the per-generator guards.
The library validates these in one place with one message shape, and a
reader who learns "a constant the guard rejects" has learned all of them.

JD015 reports an AnyString chain whose constant constraints admit no
value. This is the case ADR-0035 names by hand as the one an analyzer
should carry and the type system cannot.

Dogfooding corrected JD015 twice, in opposite directions, and both
corrections are now pinned by tests citing the sites that produced them.
Casing is not a character set: LowerCase/UpperCase constrain the case of
a fragment's LETTERS and say nothing about its other characters, so
UpperCase().StartingWith("ORD-") is legal — the first version modelled
casing as a pool and flagged a chain AnyStringTests asserts is legal. And
the length budget, which the library does compute, no longer applies once
OneOf is declared: the fragments are then matched against the pooled
values rather than laid out side by side, which is why
OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba") is legal.

Both were verified against the built library before being encoded, not
inferred from reading the guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
@Reefact
Reefact merged commit c3d8a09 into main Jul 29, 2026
28 checks passed
@Reefact
Reefact deleted the claude/justdummies-analyzers-analysis-z9q1xd branch July 29, 2026 15:22
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.

2 participants