import { mergeFlatPermissions } from "#src/permission-merge"; import type { RuleOrigin } from "#src/rule"; import type { FlatPermissionConfig, ScopeConfig } from "#src/types"; /** Surface → (pattern → originating scope). */ type OriginMap = Map>; /** Result of merging permission objects across scopes with provenance tracking. */ export interface MergedScopes { /** Fully merged flat permission config (lowest → highest precedence). */ mergedPermission: FlatPermissionConfig; /** Maps each surface to a per-pattern origin (which scope contributed it). */ origins: OriginMap; } /** * Merge permission objects across scopes (lowest → highest precedence) while * tracking which scope contributed each (surface, pattern) entry. * * Mirrors mergeFlatPermissions() semantics for origin attribution: * - Both base and incoming are objects → shallow-merge: each incoming pattern * is attributed to this scope; patterns the higher scope does not redefine * keep their earlier origin. * - Otherwise → full replacement: this scope takes over the entire surface * entry, discarding all lower-scope attribution. */ export function mergeScopesWithOrigins( scopes: readonly (readonly [RuleOrigin, ScopeConfig])[], ): MergedScopes { const origins: OriginMap = new Map(); let mergedPermission: FlatPermissionConfig = {}; for (const [scopeName, scope] of scopes) { if (!scope.permission) continue; for (const [surface, value] of Object.entries(scope.permission)) { const baseVal = mergedPermission[surface]; /* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive null/type checks; config values may differ at runtime */ const bothObjects = typeof baseVal === "object" && baseVal !== null && typeof value === "object" && value !== null; /* eslint-enable @typescript-eslint/no-unnecessary-condition */ if (bothObjects) { // Shallow-merge: each incoming pattern is attributed to this scope; // existing patterns from lower scopes keep their earlier origin. if (!origins.has(surface)) origins.set(surface, new Map()); for (const pattern of Object.keys(value)) { origins.get(surface)?.set(pattern, scopeName); } } else { // Full replacement: this scope takes over the entire surface entry. const surfaceOrigins = new Map(); if (typeof value === "string") { surfaceOrigins.set("*", scopeName); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive null check } else if (typeof value === "object" && value !== null) { for (const pattern of Object.keys(value)) { surfaceOrigins.set(pattern, scopeName); } } origins.set(surface, surfaceOrigins); } } mergedPermission = mergeFlatPermissions(mergedPermission, scope.permission); } return { mergedPermission, origins }; }