Skip to content

fix: deprecated snake-case twins overwrite explicit values (camelKey collision)#335

Merged
markdumay merged 2 commits into
mainfrom
fix/deprecated-default-collision
Jul 12, 2026
Merged

fix: deprecated snake-case twins overwrite explicit values (camelKey collision)#335
markdumay merged 2 commits into
mainfrom
fix/deprecated-default-collision

Conversation

@markdumay

Copy link
Copy Markdown
Collaborator

Summary

Structures that declare a kebab-case argument plus a deprecated snake_case
twin (e.g. show-preview + show_preview, a common pattern across Hinode)
silently overwrote an explicit value with the twin's inherited default.

Defect

Both names camelize to the same camelKey (showPreview). In
ArgsSchema.html, the deprecated twin's node inherited the canonical
argument's global definition — including its default — via the
snake→kebab normKey lookup against the module's global _arguments.yml.
In Args.html's schema loop, Go template map iteration visits keys in
sorted order, and the twin sorts after the canonical (_ > - in ASCII).
Since the loop unconditionally overwrote $out[$node.camelKey] on every
non-nil result, the twin's defaulted value clobbered the caller's explicit
show-preview=false, turning it into true. The same unconditional
last-write-wins also flipped results whenever both spellings were
explicitly provided, regardless of intended precedence. Both the strict
Args.html path and the InitArgs.html compatibility shim were affected
(the shim delegates to Args.html).

Fix (two layers)

  1. ArgsSchema.html (compile-node): a node carrying deprecated never
    stores default or config — whether inherited from the global
    definition via the normKey lookup or declared inline. A deprecated
    alias exists only to accept an explicit legacy value and warn; defaults
    belong to the canonical replacement. This also keeps a deprecated twin
    out of defaulted when the caller leaves it unset.
  2. Args.html (main schema loop): the camelKey merge is now
    precedence-safe and order-independent via a small providedKeys
    tracking dict: provided-canonical > provided-deprecated > defaulted. A
    defaulted value never overwrites an existing entry; a provided value
    overwrites a defaulted entry; a provided value from a deprecated node
    never overwrites an existing provided entry.

Golden evidence

New twins test group (exampleSite/data/structures/test-twin.yml +
exampleSite/data/tests/twins.yml) covers an inline-declared twin pair
(flag-a/flag_a) and a pair that inherits its default from the module's
global _arguments.yml (show-preview/show_preview, verified via grep
to carry default: true).

Pinned (buggy) golden — explicit-false-on-canonical:

"showPreview": true,          // should be false
"defaulted": ["show_preview"] // twin should never default

Fixed golden — same case:

"showPreview": false,
"defaulted": []

both-spellings-provided (kebab false + snake true) flips from
flagA/showPreview: true (twin's explicit value wins, wrong) to false
(canonical wins, correct) on both the args (strict) and initargs (shim)
paths.

unset-uses-canonical-default keeps flagA/showPreview at their
canonical default (true) but the defaulted list drops show_preview,
leaving only the canonical show-preview/flag-a entries.

explicit-value-on-deprecated-twin (deprecation warning + explicit twin
value honored) is unchanged before/after — it was already accidentally
correct, and is now correct by design via the precedence lattice.

Other-golden drift: none. git diff --stat tests/golden/ after the fix
touches only twins.json; all 12 pre-existing golden groups
(bookshop, casting, child, defaults, envelope, frontmatter, inittypes,
nesting, options, positional, required, schema) are byte-identical.

Test plan

  • pnpm test:update && pnpm test — 13/13 golden groups pass
  • Inspected the characterization golden (commit 1) to confirm it pins
    the exact defect described above
  • Inspected the fixed golden (commit 2) to confirm correct behavior on
    all four twin cases, on both the strict and shim paths
  • Diffed every other golden file — zero unexpected drift
  • README.md updated with a new "Deprecated arguments and camelKey
    collisions" subsection under "Argument validation"

This ships as v6.0.1 (bug fix, no API change).

🤖 Generated with Claude Code

markdumay and others added 2 commits July 12, 2026 13:44
Add a 'twins' test group covering deprecated snake_case arguments that
collide with their kebab-case canonical twin on the same camelKey: an
inline-declared pair (flag-a/flag_a) and a pair mirroring the real-world
shape where the canonical inherits a default from the module's global
_arguments.yml (show-preview/show_preview).

The pinned golden shows the defect: an explicit `show-preview=false`
is silently overwritten to `true` because the unset deprecated twin
inherits the canonical's global default and sorts after it (underscore
> hyphen) in the schema loop, and `show_preview` incorrectly appears in
`defaulted`. A second case shows the same unconditional last-write-wins
ordering flips the result when both spellings are explicitly provided.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A deprecated snake_case twin (e.g. show_preview) and its kebab-case canonical
(show-preview) camelize to the same output key. ArgsSchema.html's snake->kebab
normKey lookup let the twin inherit the canonical's global default, and
Args.html's schema loop iterates map keys in sorted order, so the twin
(sorting after the canonical, since '_' > '-') silently overwrote an explicit
show-preview=false with its inherited default of true.

Two changes fix this:

- ArgsSchema.html: a node carrying "deprecated" never stores "default" or
  "config", whether inherited from the global definition or declared inline.
- Args.html: the camelKey merge is now precedence-safe and order-independent.
  Provided-canonical values beat provided-deprecated values, which beat
  defaulted values, and a defaulted value never overwrites an existing entry.

Explicit values now always win over any default, on both the strict Args.html
path and the InitArgs.html compatibility shim. Verified against a new "twins"
golden fixture covering both an inline-declared twin pair and a pair that
inherits its default from the module's global _arguments.yml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markdumay markdumay merged commit 1fddc98 into main Jul 12, 2026
11 checks passed
@markdumay

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 6.0.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant