Skip to content

Add JD025-JD028, pin the behaviours the JD rules claim, harden the harness - #377

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

Add JD025-JD028, pin the behaviours the JD rules claim, harden the harness#377
Reefact merged 4 commits into
mainfrom
claude/justdummies-analyzers-analysis-z9q1xd

Conversation

@Reefact

@Reefact Reefact commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Four new rules for mistakes the run time either reports too late or cannot report at all, a new JustDummies.Composition category, and — following @Reefact's observation on the sweep results — an audit of the whole JD catalogue against the library's own suite, filling the four behavioural claims that turned out unpinned. A third commit hardens the analyzer test harness so a snippet that does not compile fails instead of silently passing. A fourth proposes ADR-0061, which asks whether the JD analyzers should run on this repository's own code.

Rebased onto main after #376.

Type of change

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

Changes

1. The four rules

Rule Severity What it catches What the run time does
JD025 DuplicatePoolValue 🟠 Warning Any.OneOf(1, 2, 1) nothing — the duplicate collapses silently
JD026 EmptyRelativeUri 🟠 Warning Relative().WithPathSegments(0) with no query, fragment or root throws, but inside Generate() — at act time
JD027 UnusedCombineOperand 🟠 Warning an operand the composer never reads nothing — the composed value is well-formed
JD028 InertDistinctness 🟠 Warning Distinct() over a type with no value equality nothing — from its side the requirement is met

New JustDummies.Composition category for JD027/JD028: they do not constrain a generator, they assemble several. EqualityFacts decides value equality the way the default equality comparer decides it. 48 new analyzer tests (198 → 246), 8 documentation pages (EN + FR), both index tables, release-tracking rows.

2. Pinning the behaviours the rules claim

The sweep turned up two JD025 hits, both in the library's own tests for duplicate collapsing. @Reefact's point: those tests should exist, and two is a suspiciously small number — every rule is a claim about the library, and each claim should have a test that writes the weird case and asserts the consequence.

The audit says the catalogue is in better shape than that suggests, and worse in four specific places. Already pinned: the empty relative URI (AnyUriTests.EmptyRelativeFailsAtGeneration, plus a property), the collapsing pool, the inert constraint (AnyOneOfTests.ExcludingAnAbsentValueRemovesNothing is literally JD024's shape), and the whole conflict family. Four claims were not pinned — all four silent behaviours, which is why nothing missed them:

Behaviour now pinned
JD006 building a generator draws nothing — a counting projection left unmaterialized runs zero times
JD005 a generator interpolated into text renders its type name; asserted against typeof(AnyInt32).ToString(), so it survives a rename and goes red if a ToString override ever masks the mistake
JD027 Combine draws every operand, including one the composer ignores — both counters reach 1
JD028 six "distinct" Percentage elements over a two-value domain succeed holding repeats (not statistical: six draws from a domain of two cannot show more than two values)
JD013 Any.OneOf(held) is a pool of one: the draw is the list itself, asserted by reference identity, contrasted with ElementOf on the same argument

Each carries a SuppressMessage naming its rule and saying why the shape is deliberate.

Per the suite's own rule (WritingJustDummiesTests.en.md: "a property that returns true for the wrong reason passes silently and proves nothing"), each of the five assertions was inverted once and confirmed red before being restored. The net472 floor leg builds.

3. The harness

A snippet that does not compile binds no operations, so every rule stands down and an "expects nothing" assertion passes for the wrong reason. Turning the check on immediately caught a pre-existing JD006 test whose snippet declared a static class as its own return type and instantiated it — two CS errors, green since the day it was written. Its intent is sound; only the snippet was broken, so the class becomes sealed rather than static.

It also caught a live false positive in this very wave: a throw-only composer lambda gives inference nothing to infer TResult from, so my first JD027 test did not compile, no lambda was bound, and the rule stood down for a reason that had nothing to do with the guard under test. The test passed while the real site still fired.

4. ADR-0061 — proposed, nothing wired

File: doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md (and its French translation), in the Files changed tab.

The SuppressMessage attributes above are documentary today, because the JD analyzers are loaded by no project in this repository. ADR-0061 asks whether that should change.

The argument, in short: a rule is a claim about the library, and its unit suite proves only that it fires on a snippet its own author wrote for it — so a wrong model of the library passes both sides of that test. Every model error listed below did pass its unit suite. What caught them was realistic code, and the hand-run sweep that reads it depends on whoever is working remembering to run it.

Measured before the record was written, not assumed. Loading the analyzers into JustDummies.UnitTests: the build succeeds, exactly two diagnostics are reported (the two duplicate-collapsing tests), the five suppressions added in this branch do silence their rules, and two more attributes bring the surface to zero. The other JustDummies-consuming projects report nothing. A cold non-incremental build of that project moved from ~6.6 s to ~9.3 s.

Three alternatives are argued and declined — keeping the sweep manual, a JustDummies.Usage sample mirroring FirstClassErrors.Usage, and an advisory CI job on the ADR-0046 model. The risks are stated rather than smoothed over: this verifies the loud rules and leaves the three Info ones and the two opt-in ones unexercised, which the follow-up actions carry.

Proposed only — nothing is wired in this PR, and I have not touched a status.

Dogfooding corrected the model twice in this wave

Neither error was visible from reading the source.

JD028 first claimed any sealed class without equality. That premise — every draw is a new instance — is false for a pool: Any.OneOf hands back the very references it was given, so Any.SetOf(Any.OneOf(first, second)) is legal and meaningful, and AnyOneOfTests.ReferenceIdentityKeepsDistinctInstancesDistinct asserts exactly that. The rule now fires only where the chain provably builds the value at the call site.

JD027 had to stand down on a composer whose body is a throw — and the first version of that guard did not work. An expression-bodied arrow-throw is a Return carrying the throw, through a conversion to the composer's result type. Both facts had to be measured against the operation tree.

Sweep

Across JustDummies.UnitTests, JustDummies.PropertyTests, JustDummies.Xunit.UnitTests, FirstClassErrors.Testing.UnitTests, FirstClassErrors.UnitTests, FirstClassErrors.Usage, FirstClassErrors.RequestBinder.UnitTests and FirstClassErrors.Cli.UnitTests: two hits, both JD025, both the duplicate-collapsing tests discussed above. A planted positive control fires all four.

Testing

  • dotnet build FirstClassErrors.sln — 0 warnings, 0 errors
  • dotnet test FirstClassErrors.sln — 2 168 tests, 0 failures across all 13 test projects
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests, 132) — the rules added here live in JustDummies.Analyzers.UnitTests (246); JustDummies.UnitTests goes 632 → 637
  • dotnet build JustDummies.UnitTests -c Release -f net472 -p:EnableNet472Floor=true — the floor leg the new tests have to survive
  • dotnet format style --diagnostics IDE0028 --verify-no-changes — clean on all three touched projects
  • Every new library assertion inverted once and confirmed red

Mutation testing was not run locally; the per-pull-request mutation checks are green on CI.

Documentation

  • Public API / error documentation updated — no public API change
  • README / doc/ updated — JD025JD028 pages, the rule index, the new Composition section, and ADR-0061
  • French translation updated — the four .fr.md rule pages, the French index, and the ADR's .fr.md
  • No documentation change required

Architecture decisions

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

ADR-0061, Run the JustDummies analyzers on the repository's own code, is drafted as Proposed and indexed. No status flipped, no wiring done.

The rules themselves embark no architectural decision: ADR-0059 covers the recipe-versus-value boundary, and adding a category is a routine consequence of adding rules. The harness change is an implementation improvement.

Remaining

This completes the six rules I could build alone. Two are left, and both need your decision: the unsupported-regex-literal rule (the RegexParser grammar would have to be shared with the analyzer through a linked source file) and the Any-outside-a-test-assembly rule (needs an IsTestProject oracle, with FirstClassErrors.Testing as a live counterexample — a shipping package that legitimately references JustDummies).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq

claude added 3 commits July 29, 2026 17:24
A snippet that does not compile binds no operations, so every rule stands
down and an "expects nothing" assertion passes for the wrong reason. The
harness returned the analyzer diagnostics without ever looking at the
compiler's, which made that failure mode invisible.

It is not hypothetical. Turning the check on immediately caught a JD006
test whose snippet declared a static class as its own return type and
instantiated it — two CS errors, green since the day it was written. The
test's intent is sound (a lookalike API from another library must not be
reported); only its snippet was broken, so the class becomes sealed
rather than static and the shape it was testing is unchanged.

The same check caught a live false positive in the wave being written
against it, which is what prompted this: a composer lambda cannot infer
its result type from a throw-only body, so the snippet did not compile,
no lambda was bound, and the rule stood down for a reason that had
nothing to do with the guard under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
Four rules for mistakes the run time either reports too late or cannot
report at all, and a new JustDummies.Composition category for the two
that are about assembling generators rather than constraining one.

JD025 DuplicatePoolValue (Warning) — the same constant listed twice in a
pool. Pools are deduplicated at construction, so the duplicate does
nothing; the reading it exists to refuse is weighting, which the library
declines on purpose. Nothing fails here, and that is the problem: the
consequence lands elsewhere, when a distinct collection over the pool
gates against the real distinct count and names a number the author
cannot find in their source.

JD026 EmptyRelativeUri (Warning) — a relative URI with zero path
segments and no query, fragment or root. This is the one member of the
constraint family whose failure lands at act time rather than at the
arrange line: emptiness is only settled once the components are drawn,
so the library throws inside Generate() with a stack pointing at the
code under test. Moving it to build time is the whole value.

JD027 UnusedCombineOperand (Warning) — an operand the composer never
reads. Combine draws every part before composing, so the operand is
generated, constraints and all, then dropped. Name the parameter '_' to
say the draw is deliberate.

JD028 InertDistinctness (Warning) — distinctness over an element type
with no value equality. The default comparer falls back to reference
equality and every built element is a new instance, so the requirement
is met by construction and constrains nothing. Measured: six "distinct"
elements over a two-value domain came back as [1, 1, 1, 2, 1, 2], green.

Dogfooding corrected the model twice, and neither error was visible from
the source alone.

JD028 first claimed any sealed class without equality. That premise —
every draw is a new instance — is false for a pool: Any.OneOf hands back
the very references it was given, so Any.SetOf(Any.OneOf(first, second))
is legal and meaningful, and the library's own suite asserts exactly
that. The rule now fires only where the chain provably builds the value
at the call site.

JD027 stood down on a composer whose body is a throw, and the first
version of that guard did not work: an expression-bodied '=> throw ...'
is a Return CARRYING the throw, through a conversion to the composer's
result type. Both facts had to be measured.

Sweep across JustDummies.UnitTests, JustDummies.PropertyTests,
JustDummies.Xunit.UnitTests, FirstClassErrors.Testing.UnitTests,
FirstClassErrors.UnitTests, FirstClassErrors.Usage and the binder and
CLI suites: two hits, both JD025, both in the library's own tests for
duplicate collapsing — code that writes the flagged shape on purpose
because it is the test for it. A planted positive control fires all
four.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
Every JD rule is a claim about how the library behaves. Auditing the
catalogue against the suite found the claims mostly pinned already — the
empty relative URI, the collapsing pool, the inert constraint and the
whole conflict family all have their example — and four that were not.
All four are silent behaviours, which is why nothing missed them.

Each new test writes the shape its rule reports, on purpose, and asserts
the consequence. A SuppressMessage names the rule and says why the shape
is deliberate, so a reader meeting the test after the analyzers are wired
in finds the reason rather than a puzzle.

JD006 — building a generator draws NOTHING. A counting projection built
and left unmaterialized runs zero times. This is why a chain left without
Generate() is silent: the arrange line reads like it did something.

JD005 — a generator interpolated into text renders its type name. The
assertion is against typeof(AnyInt32).ToString(), so it survives a rename
and goes red the day someone adds a ToString override returning a drawn
value — an override that would mask the mistake is exactly what this
pins the absence of.

JD027 — Combine draws every operand before composing, including one the
composer never reads. Two counting operands, a composer reading one:
both counters reach 1.

JD028 — distinctness over an element type without value equality is
inert. Percentage has no Equals and '.As' builds a new instance per draw,
so six "distinct" elements over a two-value domain succeed while holding
repeats. Not statistical: six draws from a domain of two cannot show
more than two values.

JD013 — a held collection passed to OneOf is ONE pool member. Inference
binds T to the collection, so the call is legal, the draw succeeds, and
what comes back is the whole list; the test asserts reference identity
with the argument, and contrasts it with ElementOf on the same list.

Each assertion was inverted once and confirmed red before being restored,
per the suite's own rule that a test which cannot fail proves nothing.
The net472 floor leg builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqyhG9Y4AfdxEYekP2Evq
@Reefact
Reefact force-pushed the claude/justdummies-analyzers-analysis-z9q1xd branch from 96efa1a to 9114bd4 Compare July 29, 2026 17:27
@Reefact Reefact changed the title Add JD025-JD028 and fail analyzer snippets that do not compile Add JD025-JD028, pin the behaviours the JD rules claim, harden the harness Jul 29, 2026
The JustDummies analyzers are loaded by no project in this repository.
Their only standing verification is their own unit suite, where the same
author writes both the rule and the snippet that tests it — so a wrong
model of the library passes both sides.

The evidence that this matters is already in the log. A hand-run sweep
over the repository's suites has contradicted that model at least once in
each of the last four waves: JD015's casing-as-a-character-pool, JD016
counting declared enum members under AllowingCombinations, JD023 on the
representable extremes, JD028 assuming every draw is a fresh instance,
and JD027's throw guard that did not fire. None was found by the unit
suite. All were found on code written for other reasons, which is exactly
why it did not share the misconception.

The record proposes loading the analyzers into every project that
consumes JustDummies, and consigning each deliberate violation to a
suppression naming its rule.

Measured before writing, not assumed: the build succeeds, exactly two
diagnostics are reported, the five suppressions added in this branch
already silence their rules, and two more attributes bring the surface to
zero. A cold non-incremental build of that project moved from roughly six
and a half seconds to nine.

Three alternatives are argued and declined — keeping the sweep manual, a
JustDummies sample mirroring FirstClassErrors.Usage, and an advisory CI
job on the ADR-0046 model. The risks are stated rather than smoothed
over: this verifies the loud rules and leaves the three Info ones and the
two opt-in ones unexercised, which the follow-up actions carry.

Proposed only. The maintainer accepts or declines; nothing is wired.

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