Deep-merge semantics, precisely
The whole algorithm fits in three rules. When a key holds an object on both sides, the two objects merge key by key, and the same rules apply one level down. When a key exists on only one side, it is kept as is. In every other case — scalar vs scalar, array vs array, or mismatched types — the Override value replaces the Base value. There is no third state and no heuristic: the same two documents produce the same result every time.
The rules compose in useful ways. Type conflicts resolve wholesale: if the Base holds an object at a key and the Override holds a string, the string wins, and the merge never tries to reconcile across types. An explicit null in the Override counts as a value — it overwrites the Base entry with null but keeps the key. That is a deliberate difference from JSON Merge Patch (RFC 7386), where null means "delete this key". This tool applies overlay semantics, not patch-with-deletion semantics.
Two output details are worth knowing. The result is pretty-printed with two-space indentation, ready to paste into a config file. And key order is stable: the Base keys come first in their original order, with keys that only exist in the Override appended after them — which is why debug lands at the end of the example above.