R2: the Null Object is named and no argument is nil - #29
Merged
Conversation
The 2.10.1 example defaulted a nil sink inside the constructor
(if sink == nil { sink = DiscardSink() }), which keeps NewReporter(nil) legal
and only relocates the nil-check. The first re-run of case-e-nils-refactor on
2.10.1 produced exactly that shape: io.Discard composed inline, no named
DiscardSink, and a test still calling NewReporter(nil, ...). The example now
supplies DiscardSink() through an option, the fix move says the default is
named and passed (never inferred from nil), and the changelog records it under
Unreleased.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WrQTEgY87C1UdfgejismGr
…gent applying it reads
The second case-e refactor re-run on 2.10.1 reached the option-based constructor
with no nil anywhere, then introduced a one-implementation Sink interface with a
noopSink{} for the no-op. The R11 and R2 rule files already forbid that, but the
refactoring skill's move table only names the move and points at the rule. The
table now lists Introduce Null Object under R2 for optional collaborators and
carries the Go shape beneath it: a named value of the existing concrete type
(DiscardSink() composing io.Discard) supplied through an option or passed by
name; never a new interface for the no-op (R6), never a nil parameter meaning
default (R2).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WrQTEgY87C1UdfgejismGr
buzzdan
marked this pull request as ready for review
September 11, 2026 09:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #28, from the first re-run of the case-e evals on 2.10.1.
The 2.10.1 R2 example defaulted a nil
sinkinside the constructor:That keeps
NewReporter(nil)legal and only relocates the nil-check. The re-run ofcase-e-nils-refactor(two passes) showed both halves of the gap:io.Discardcomposed inline in the constructor, no namedDiscardSink(), a test still callingNewReporter(nil, ...).Sinkinterface with anoopSink{}for the no-op, which R11 and R2 already forbid but the refactoring skill's move table only names the move and points at the rule.The review side did move:
fix-null-objectwent 0/2 → 2/2 and one run wrotefunc DiscardSink() *Sink { return NewSink(io.Discard) }verbatim.Changes (regenerated into the plugin):
core/rules/R2-self-validating-types.md: the design point says no parameter accepts nil to mean "default", names the constructor guard as the smell in a new place, and the example suppliesDiscardSink()andtime.Nowthrough an option:NewReporter(opts ...Option),NewReporter(WithSink(sink))in production,NewReporter(WithClock(fixed))in tests. The Introduce Null Object fix move says the default is named and passed or supplied by an option, never inferred from nil.core/skills/refactoring/SKILL.md: the move table lists Introduce Null Object under R2 for optional collaborators and carries the Go shape beneath it: a named value of the existing concrete type (DiscardSink()composingio.Discard) supplied through an option or passed by name; never a new interface for the no-op (R6), never a nil parameter meaning default (R2).task check,lint-core(Residue table refreshed) and the docs gate pass.Companion grader change: buzzdan/ldd-evals#3.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WrQTEgY87C1UdfgejismGr