Skip to content

Canonicalize bundle reason: sort consumers and dedupe files - #170

Merged
ChALkeR merged 1 commit into
mainfrom
claude/reason-arrays-sorting-j5teou
Aug 21, 2026
Merged

Canonicalize bundle reason: sort consumers and dedupe files#170
ChALkeR merged 1 commit into
mainfrom
claude/reason-arrays-sorting-j5teou

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Summary

This change ensures that bundle reason metadata is emitted in a canonical form—with consumers sorted alphabetically and file lists deduplicated and path-sorted—preventing the serialized artifact bytes from depending on merge history or discovery order.

Key Changes

  • mergeReason() function: Refactored to produce canonical output by:

    • Sorting consumer keys alphabetically
    • Deduplicating and sorting file paths within each consumer's list
    • Using a Map internally to safely handle all consumer names (including __proto__)
    • Filtering out non-array values (unvalidated informational data)
    • Returning undefined when both inputs are undefined
  • Bundle.serialize(): Now canonicalizes the reason field by passing it through mergeReason(), ensuring that parsed artifacts don't leak stale record order into the serialized bytes

  • Test coverage: Added three new tests validating:

    • Bundle.merge() emits reason consumers in sorted order, not merge order
    • Bundle.withReason() emits path-sorted file lists, not discovery order
    • Bundle.serialize() canonicalizes reason with sorted consumers and deduplicated, sorted files
  • Comment clarification in add.js: Updated to reflect that sorting is for deterministic diagnostics, with canonicalization handled by serialize()

Implementation Details

The mergeReason() function now uses Map to accumulate consumer→file-set mappings, then converts back to a plain object with sorted keys and deduplicated, sorted file arrays. This ensures consistent output regardless of input order or whether values come from a fresh stamp, a merge, or a parsed artifact.

https://claude.ai/code/session_01SDjoKAkSVQ4cfurL6RpuB1

The informational `reason` map's shape depended on how it was produced:
a fresh withReason() stamp kept the file list in scan-traversal (or CLI
arg) order, so any change to the import graph reshuffled the committed
bundle's `reason` arrays; merges appended new consumers instead of
sorting them; and only state's #bundleReason emitted a canonical form,
so rewrites through a different path reordered what the previous one
wrote.

Make mergeReason always return the canonical form -- consumers sorted,
each file list deduped and path-sorted, matching #bundleReason -- and
have serialize() canonicalize `reason` like every other field instead
of emitting it verbatim, so a parsed artifact's stale order can't leak
back into the bytes. The union now accumulates in a Map (a plain-object
'__proto__' consumer key would hit the prototype) and drops a non-array
list, since parse leaves the informational field unvalidated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDjoKAkSVQ4cfurL6RpuB1
@ChALkeR
ChALkeR merged commit 838d8a7 into main Aug 21, 2026
5 checks passed
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.

3 participants