fix(lease): restore uniform settings array replacement

AMD1213-B1: preserve the gated Claude hook composition explicitly in the lease overlay while restoring last-layer-wins arrays and null tombstones.
This commit is contained in:
terra
2026-08-13 14:38:19 -05:00
parent cb960237d3
commit 9de9ffa56b
3 changed files with 162 additions and 26 deletions
@@ -253,12 +253,9 @@ function cloneValue(value: unknown): unknown {
return value;
}
type MergeContext = 'root' | 'hooks' | 'nested';
function mergeObject(
lower: Record<string, unknown>,
higher: Record<string, unknown>,
context: MergeContext = 'nested',
): Record<string, unknown> {
const result = cloneValue(lower) as Record<string, unknown>;
for (const [key, highValue] of Object.entries(higher)) {
@@ -267,36 +264,23 @@ function mergeObject(
continue;
}
const lowValue = result[key];
if (context === 'hooks' && Array.isArray(lowValue) && Array.isArray(highValue)) {
result[key] = [...(lowValue as unknown[]), ...(cloneValue(highValue) as unknown[])];
continue;
}
result[key] =
isPlainObject(lowValue) && isPlainObject(highValue)
? mergeObject(
lowValue,
highValue,
context === 'root' && key === 'hooks' ? 'hooks' : 'nested',
)
? mergeObject(lowValue, highValue)
: cloneValue(highValue);
}
return result;
}
/**
* Deep object merge. Scalars and arrays replace; null in a higher layer
* deletes. Exception: hook event arrays directly under the top-level `hooks`
* key concatenate (base entries first), so an overlay ADDS gating without
* erasing the base QA hooks that share an event — replacing them would make
* the gap-7 base/overlay split unimplementable without duplicating base
* hooks inside the lease overlay. Removing an event entirely still works via
* the null tombstone.
* Deep object merge. Objects merge recursively; scalar and array conflicts
* resolve last-layer-wins; null in a higher layer deletes the key.
*/
export function deepMergeSettings(
...layers: ReadonlyArray<Record<string, unknown> | undefined>
): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>(
(merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer, 'root')),
(merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer)),
{},
);
}