Development record
2026-08-06 — iss-187 (bug-hunt loop, round 1): `rules.Merge`
2026-08-06 — iss-187 (bug-hunt loop, round 1): rules.Merge
(internal/core/rules/rules.go) assigned override domain keys straight into
out.Domains without ever allocating that map, because its cloneRuleSet
helper deliberately preserves a nil Domains (it only allocates when the
source map is non-nil). A base such as RuleSet{SchemaVersion: 1} — which
Validate accepts — therefore panicked with "assignment to entry in nil map"
the moment the overlay carried a domain, contradicting Merge's own doc
comment ("New domain keys are added"), which states no such precondition. Not
reachable in production today: the sole call site, RuleSet.Load, always
passes Merge(Defaults(), over) and Defaults() always has a populated map —
so this was a latent defect in an exported API contract that would go live for
the first caller merging onto a non-Defaults() base (a future multi-tier
overlay starting from an empty set). Fixed at the Merge call site by
allocating out.Domains when it is nil and the overlay has at least one key,
matching the idiom the sibling loader guard.Merge
(internal/core/guard/config.go) already uses for out.Entries; cloneRuleSet
keeps its nil-preserving semantics, so no other caller's behaviour moves.
Repro: internal/core/rules/rules_test.go,
TestMergeNilBaseDomainsAddsNewKeys, watched failing on pre-fix code for the
claimed reason (panic: assignment to entry in nil map) and passing after.
Frontmatter
| id | dcn-2608060000000003 |
|---|---|
| type | decision |
| date | 2026-08-06 |
.abcd/work/decisions/dcn-2608060000000003-iss-187-bug-hunt-loop-round-1-rules.md
open on GitHub ↗ · commit history ↗
mentions1
- iss-187rules.Merge panics with 'assignment to entry in nil map' (rules.go:193) when the base RuleSet's Domains field is nil and the overlay has at least one domain key. cloneRuleSet (rules.go:524) only allocates out.Domains when the source map is already non-nil, so a base like RuleSet{SchemaVersion: 1} (a Validate-accepted, valid zero-ish value) crashes on merge, contradicting Merge's own doc comment ('New domain keys are added') which promises no such precondition. The sibling loader guard.Merge (internal/core/guard/config.go:77-79) gets this right, explicitly allocating out.Entries when nil before the same kind of loop — Merge is simply missing that guard. Not reachable today: RuleSet.Load's only call site passes Merge(Defaults(), over), and Defaults() always has non-nil Domains, so this is a latent exported-API contract defect rather than a live crash — it becomes live the moment any caller merges onto a non-Defaults() base (e.g. a future multi-tier overlay starting from an empty RuleSet). Reproducing test (recover-guarded): internal/core/rules/, TestMergeNilBaseDomainsAddsNewKeys.