/** * Predicate Resolver * * Maps predicate names to existing compiled DSL rules across all graph scopes. * Does NOT create new rules - only looks up existing ones. */ export class PredicateResolver { constructor(context) { this.context = context; this.graphStores = context.graphStores || {}; this.cache = new Map(); } /** * Resolve a predicate name to its DSL rule * * @param {string} predicateName - Name of the predicate * @returns {Object|null} Rule info or null if not found */ resolve(predicateName) { // Check cache first if (this.cache.has(predicateName)) { return this.cache.get(predicateName); } // Look up in all graph scopes const rule = this.findRule(predicateName); if (rule) { this.cache.set(predicateName, rule); } return rule; } /** * Find a rule across all graph scopes * Prefers logical rules over direct rules for evidence predicates */ findRule(predicateName) { const scopes = [ 'tenantExternal', 'tenantInternal', 'rootExternal', 'rootInternal', 'masterExternal', 'masterInternal' ]; let directRule = null; let directScope = null; for (const scopeName of scopes) { const graphStore = this.graphStores[scopeName]; if (!graphStore) { continue; } // Handle both: Arbiter directly (has .check()) or wrapper with .arbiter const arbiter = graphStore.arbiter || graphStore; const config = arbiter.relationConfigs?.get(predicateName); if (config) { // Prefer logical rules (intersection/union) over direct rules // This ensures evidence rules work correctly across all scopes if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') { return { name: predicateName, scope: scopeName, config: config, arity: this.inferArity(config) }; } // Remember the first direct rule as fallback if (!directRule && config.type === 'direct') { directRule = config; directScope = scopeName; } } } // Return direct rule if no logical rule found if (directRule) { return { name: predicateName, scope: directScope, config: directRule, arity: this.inferArity(directRule) }; } return null; } /** * Infer the arity (parameter count) from rule config */ inferArity(config) { // Most DSL evidence rules have 1 or 2 parameters: // - 1 param: just the subject (user) // - 2 params: subject (user) + object if (config.arity) { return config.arity; } // Default to checking if it's a relation that typically needs an object // This is a heuristic - in practice, the DSL defines this explicitly if (config.type === 'tuple_to_userset' || config.type === 'direct') { return 2; // Likely needs subject + object } return 1; // Default to 1 param } /** * Check if a predicate exists without full resolution */ exists(predicateName) { return this.resolve(predicateName) !== null; } /** * Get all available predicates across all scopes */ getAllPredicates() { const predicates = []; const scopes = [ 'tenantExternal', 'tenantInternal', 'rootExternal', 'rootInternal', 'masterExternal', 'masterInternal' ]; for (const scopeName of scopes) { const graphStore = this.graphStores[scopeName]; if (!graphStore) { continue; } // Handle both: Arbiter directly (has .check()) or wrapper with .arbiter const arbiter = graphStore.arbiter || graphStore; if (arbiter.relationConfigs) { for (const [name, config] of arbiter.relationConfigs) { predicates.push({ name, scope: scopeName, arity: this.inferArity(config) }); } } } return predicates; } /** * Clear the cache (useful for testing or when rules change) */ clearCache() { this.cache.clear(); } }