import { ContextMap, ContextScope } from '../context'; import { JsonValue } from '../json'; /** * A serializable boolean tree, persisted as config and consumed by * {@link RuleRegistry.compile | RuleRegistry.compile}. * * Leaves reference primitives or saved rules by id * (`{ type: 'ref'; id }`). Branches combine children with `all`, `any`, * or `not`. * * Primitives and saved rules share a single id namespace, so a `ref` * resolves to one or the other when `compile` runs. If the id belongs * to a registered primitive, the supplied `args` are bound and a * predicate is built. If it belongs to a saved rule, the saved tree * is expanded in place, any `args` on the outer `ref` are ignored at * compile time (a saved rule is already a complete tree), and * {@link RuleRegistry.validate | validate} reports the * `args-on-saved-rule` diagnostic so authoring tools can prompt for * cleanup. Registering a primitive with the same id as an existing * saved rule, or vice versa, is an error. * * Saved rules are loaded by the host client from its own config layer * at boot, there is no public API on {@link RuleRegistry} for * registering or listing them. A `ref` to an unknown id surfaces as an * `unknown-ref` diagnostic at compile or validate time. * * `all` and `any` short-circuit during evaluation: `all` stops at the * first child that returns `false`, `any` at the first that returns * `true`. Later children are not evaluated. * * @example A composite rule, deal is won and user is admin * ```ts * import { Rule } from '@limetech/lime-web-components'; * * const rule: Rule = { * type: 'all', * rules: [ * { type: 'ref', id: 'limetech.deal-status', args: { status: 'won' } }, * { type: 'ref', id: 'limetech.is-admin' }, * ], * }; * ``` * * @example Referencing a saved rule * ```ts * import { Rule } from '@limetech/lime-web-components'; * * const rule: Rule = { * type: 'not', * rule: { type: 'ref', id: 'mycorp.high-value-deal' }, * }; * ``` * * @see {@link StoredRule} for the persisted envelope. * @see {@link CompiledRule} for the runtime predicate. * @see {@link RuleRef}, {@link RuleAll}, {@link RuleAny}, {@link RuleNot} * for the individual variants. * * @beta * @group Rules */ export type Rule = RuleRef | RuleAll | RuleAny | RuleNot; /** * Leaf rule that references a registered {@link RulePrimitive} or a * saved rule by id. * * @beta * @group Rules */ export interface RuleRef { /** * Discriminant for a leaf that references a primitive or saved rule. */ type: 'ref'; /** * The id of a registered {@link RulePrimitive} or a saved rule. */ id: string; /** * Arguments passed to the primitive at compile time. Constrained * to {@link JsonValue} because a `Rule` round-trips through JSON * persistence. Ignored when the reference resolves to a saved * rule. */ args?: JsonValue; } /** * `AND` composition. Short-circuits on the first `false` child. * * @beta * @group Rules */ export interface RuleAll { /** * Discriminant for an `AND` composition. */ type: 'all'; /** * Child rules, evaluated in order. */ rules: Rule[]; } /** * `OR` composition. Short-circuits on the first `true` child. * * @beta * @group Rules */ export interface RuleAny { /** * Discriminant for an `OR` composition. */ type: 'any'; /** * Child rules, evaluated in order. */ rules: Rule[]; } /** * Negation. Inverts the truth value of the inner rule. * * @beta * @group Rules */ export interface RuleNot { /** * Discriminant for a negation. */ type: 'not'; /** * Inner rule whose truth value is inverted. */ rule: Rule; } /** * A {@link Rule} wrapped with a version number for storage. * * Persisted rules are stored in this envelope. Currently only * `version: 1` exists. When new versions are introduced, the * implementation is expected to upgrade older stored rules to the * current shape before passing them to {@link RuleRegistry.compile}, * so `compile` itself only ever sees the current version. * * @example Wrapping a rule for storage * ```ts * import { StoredRule } from '@limetech/lime-web-components'; * * const stored: StoredRule = { * version: 1, * rule: { type: 'ref', id: 'limetech.is-admin' }, * }; * ``` * * @beta * @group Rules */ export interface StoredRule { /** * Schema version of the stored rule. Always `1` for now; the type * widens when new versions are introduced. */ version: 1; /** * The inner {@link Rule} tree. */ rule: Rule; } /** * A {@link Rule} compiled into an {@link CompiledRule.evaluate | evaluate} * function plus the static {@link CompiledRule.reads | reads} it depends on. * * Returned by {@link RuleRegistry.compile}. Captures the primitives and * saved rules that were registered when `compile` ran. See the * snapshot note on {@link RuleRegistry.compile | compile}. * * Fails closed. A slot with no provider, a thrown predicate, and any * other missing data evaluate to `false`. The full contract, including * how exceptions are caught, lives on * {@link RuleRegistry.compile | compile}. * * @beta * @group Rules */ export interface CompiledRule { /** * Evaluate the rule against a scope. Fails closed: if a primitive * predicate throws or a context key has no provider, returns * `false`. See {@link RuleRegistry.compile | compile} for the full * contract. */ evaluate(scope: ContextScope): boolean; /** * The complete, de-duplicated union of context keys every * referenced primitive declares it reads, across the whole rule * tree. The full superset — not the keys one evaluation happens to * touch — so a subscriber watches every key a short-circuiting rule * could read. */ readonly reads: readonly (keyof ContextMap)[]; }