UNPKG

covertable

Version:

Efficient TypeScript library for pairwise testing, generating minimal covering arrays with constraint support.

520 lines (476 loc) 16.9 kB
/** * Arithmetic expressions compute a value from two operands. * Both `left` and `right` can be field references or nested expressions. * When `right` is omitted, `value` provides a literal operand. */ declare type ArithmeticExpression = { operator: 'add' | 'sub' | 'mul' | 'div' | 'mod' | 'pow'; left: Operand; right: Operand; } | { operator: 'add' | 'sub' | 'mul' | 'div' | 'mod' | 'pow'; left: Operand; value: any; }; declare type ArrayObjectType = { [s: string]: any[]; }; declare type ArrayTupleType = any[][]; declare type CandidateType = [ScalarType, number][]; /** * Custom comparison functions. Each key matches a comparison operator name. * When provided, the corresponding function is called instead of the default * JS comparison. The function receives two resolved (non-undefined) values * and must return a boolean. * * The `in` comparer receives the field value and the values array. * * `undefined` values (= field not yet set in the row) are **never** passed * to a comparer — the engine returns `null` before reaching the * comparer in that case. */ declare interface Comparer { eq?: (a: any, b: any) => boolean; ne?: (a: any, b: any) => boolean; gt?: (a: any, b: any) => boolean; lt?: (a: any, b: any) => boolean; gte?: (a: any, b: any) => boolean; lte?: (a: any, b: any) => boolean; in?: (value: any, values: Set<any>) => boolean; } /** * A comparison condition. `left` is the first operand (field reference or * expression). The second operand is either `right` (another field/expression) * or `value` (a literal). For `in`, use `values` (an array of literals). */ declare type ComparisonExpression = { operator: 'eq'; left: Operand; value: any; } | { operator: 'eq'; left: Operand; right: Operand; } | { operator: 'ne'; left: Operand; value: any; } | { operator: 'ne'; left: Operand; right: Operand; } | { operator: 'gt'; left: Operand; value: any; } | { operator: 'gt'; left: Operand; right: Operand; } | { operator: 'lt'; left: Operand; value: any; } | { operator: 'lt'; left: Operand; right: Operand; } | { operator: 'gte'; left: Operand; value: any; } | { operator: 'gte'; left: Operand; right: Operand; } | { operator: 'lte'; left: Operand; value: any; } | { operator: 'lte'; left: Operand; right: Operand; } | { operator: 'in'; left: Operand; values: any[]; }; declare class Controller<T extends FactorsType> { factors: FactorsType; options: OptionsType<T>; factorLength: number; factorIsArray: Boolean; private serials; private parents; private indices; incomplete: PairByKeyType; private rejected; row: Row; private _totalPairs; private _prunedPairs; private _rowCount; private _uncoveredPairs; private _completions; get stats(): ControllerStats_2; private constraints; private constraintsByKey; private comparer; /** * Indices into `constraints` that have already evaluated to `true` * against the **current** row. Cleared whenever the row is reset or * yielded. Safe because the row only grows and each condition is * deterministic over its declared keys. */ private passedIndexes; constructor(factors: FactorsType, options?: OptionsType<T>); /** Normalize `in` conditions: convert `values` arrays to Sets for O(1) lookup. */ private static normalizeCondition; private resolveConstraints; private serialize; private setIncomplete; /** * Try to add a candidate pair to the current row. Evaluates constraints * against a snapshot (row + pair) without mutating `this.row`. If all * constraints pass (or are unknown), the pair is committed to `this.row` * and `true` is returned. If any constraint definitively fails, `this.row` * is unchanged and `false` is returned. */ private setPair; private consumePairs; getCandidate(pair: PairType): CandidateType; isCompatible(pair: PairType): number | null; /** * Check whether adding `candidate` to `row` would violate any constraint. * Returns `true` (OK), `false` (definitively violated), or `null` * (some dependency is still missing — defer). * * Constraints already in `passedIndexes` are skipped. */ private storableCheck; /** * Returns the number of new keys this candidate would add to `row`, or * `null` if the candidate is incompatible or would definitively violate a * constraint. `null` results from three-valued evaluation are treated * as "OK for now" — they will be rechecked once more keys are known. */ storable(candidate: CandidateType, row?: Row): number | null; /** * Evaluate constraints against `row` and mark those that pass as done. * Returns `false` if any constraint definitively fails (= the row is * unsalvageable and should be abandoned), `true` otherwise. */ private markPassedConstraints; /** * Forward checking: given a snapshot row, propagate constraints to prune * domains of unfilled factors. If any factor's domain becomes empty, the * current assignment is unsolvable — return false. * * This is read-only: it does NOT modify this.row. It builds a temporary * domain map and iteratively narrows it by evaluating constraints with * each candidate value. */ private forwardCheck; isFilled(row: Row): boolean; private toMap; private toObject; private reset; private restore; /** * Fill the remaining unfilled factors of `this.row` and check constraints. * Uses depth-first backtracking: each unfilled factor tries its values in * order (weight-sorted on the first pass). When a value causes a * constraint to evaluate to `false` (via three-valued `storable`), the * next candidate is tried; if all candidates are exhausted, the previous * factor is backtracked. * * Returns `true` when a valid completion is found (the row is updated in * place), or `false` when no valid completion exists. */ /** * Result of close(): `true` = valid completion found; `false` = failed; * or an object with the conflict keys from the first failing constraint. */ private close; get strength(): number; get allStrengths(): number[]; private valueToSerial; private applyPreset; private orderByWeight; get isComplete(): boolean; /** * Find the keys of the first failing constraint on the current row. * Returns the set of factor keys that participate in the conflict, * or null if the row passes all constraints. */ private findConflictKeys; /** * Analyse remaining incomplete pairs and identify which constraint(s) * make each pair infeasible. Used to build a diagnostic when throwing * NeverMatch. */ private diagnoseUncoveredPairs; /** * Record which factor values were filled by close() (completion) rather * than by greedy. `greedyKeys` are the keys that were already in the row * before close() ran. */ private recordCompletions; get progress(): number; make<T extends FactorsType>(): SuggestRowType<T>[]; makeAsync<T extends FactorsType>(): Generator<SuggestRowType<T>, void, unknown>; } declare interface ControllerStats { /** Total pairs before any pruning. */ totalPairs: number; /** Number of pairs pruned by constraints (infeasible). */ prunedPairs: number; /** Number of pairs consumed so far. */ coveredPairs: number; /** Coverage ratio: coveredPairs / (totalPairs - prunedPairs). */ progress: number; /** Number of generated rows. */ rowCount: number; /** Pairs that could not be covered. Populated after make/makeAsync completes. */ uncoveredPairs: UncoveredPair[]; /** Counts of values filled by close() (completion), keyed by factor then value. */ completions: Record<string, Record<string, number>>; } declare interface ControllerStats_2 { /** Total pairs before any pruning. */ totalPairs: number; /** Number of pairs pruned by constraints (infeasible). */ prunedPairs: number; /** Number of pairs consumed so far. */ coveredPairs: number; /** Coverage ratio: coveredPairs / (totalPairs - prunedPairs). */ progress: number; /** Number of generated rows. */ rowCount: number; /** Pairs that could not be covered. Populated after make/makeAsync completes. */ uncoveredPairs: UncoveredPair_2[]; /** Counts of values filled by close() (completion), keyed by factor then value. */ completions: Record<string, Record<string, number>>; } declare type Expression = ComparisonExpression | LogicalExpression | FnExpression; declare type FactorsType = ArrayTupleType | ArrayObjectType; declare type FilterRowType = { [key: string]: any; [index: number]: any; }; declare type FilterType = (row: FilterRowType) => boolean; /** * Escape hatch for constraints that cannot be expressed declaratively. * The engine cannot perform three-valued reasoning on these — when a * dependency key is missing the condition is treated as `null`. * Provide `requires` so the engine knows when it is safe to call `evaluate`. */ declare type FnExpression = { operator: 'fn'; requires: string[]; evaluate: (row: { [key: string]: any; }) => boolean; }; declare type IndicesType = Map<number, number>; export declare type IssueSeverity = "error" | "warning"; export declare type IssueSource = "factor" | "subModel" | "constraint"; declare type LogicalExpression = { operator: 'not'; condition: Expression; } | { operator: 'and'; conditions: Expression[]; } | { operator: 'or'; conditions: Expression[]; }; /** * An operand is either a field reference (string, supports dot notation * like `"payment.method"`) or an arithmetic expression. */ declare type Operand = string | ArithmeticExpression; declare interface OptionsType<T extends FactorsType> { strength?: number; subModels?: SubModelType[]; weights?: WeightsType; presets?: PresetRowType[]; sorter?: SorterType; criterion?: (ctrl: Controller<T>) => IterableIterator<PairType>; salt?: ScalarType; tolerance?: number; /** * Declarative constraints. Each entry is a `Condition` tree that the * generator evaluates under Kleene three-valued logic: when a referenced * field is not yet present in the row the result is `null` (deferred) * rather than `false`, so the generator can prune early without * discarding viable combinations. * * The top-level array is an implicit AND: every condition must be * satisfied for a row to be accepted. */ constraints?: Expression[]; /** * Custom comparison functions. See `Comparer` for details. */ comparer?: Comparer; } declare type PairByKeyType = Map<ScalarType, PairType>; declare type PairType = number[]; /** * Parse a PICT-format model string into its constituent parts: parameters, * sub-models, and constraints. Issues are collected rather than thrown so the * caller can decide how to handle them. */ export declare function parse(input: string, options?: ParseOptions): ParseResult; export declare interface ParseOptions { /** Match constraint comparisons and alias lookups case-insensitively. Default: true. */ caseInsensitive?: boolean; } export declare interface ParseResult { factors: PictFactorsType; aliases: Map<string, string>; negatives: Map<string, Set<string | number>>; weights: { [factorKey: string]: { [index: number]: number; }; }; subModels: SubModelType[]; lexer: PictConstraintsLexer | null; issues: PictModelIssue[]; } declare class PictConstraintsLexer { private input; private debug; private aliases; private caseInsensitive; private startLine; private tokens; filters: (FilterType | null)[]; errors: (string | null)[]; filterLines: number[]; filterKeys: Set<string>[]; constructor(input: string, debug?: boolean, aliases?: Map<string, string>, caseInsensitive?: boolean, startLine?: number); private tokenize; private analyze; filter: (row: FilterRowType, ...additionalFilters: FilterType[]) => boolean; } export declare type PictFactorsType = { [key: string]: (string | number)[]; }; export declare class PictModel { private _parameters; private _subModels; private _negatives; private _weights; private _lexer; private _controller; issues: PictModelIssue[]; constructor(input: string, options?: PictModelOptions); get parameters(): PictFactorsType; get subModels(): SubModelType[]; get constraints(): Expression[]; get negatives(): Map<string, Set<string | number>>; get weights(): { [factorKey: string]: { [index: number]: number; }; }; get progress(): number; get stats(): ControllerStats | null; /** * Evaluate all constraints and the negative-value rule against a * (typically complete) row. Returns true if the row passes. */ filter: (row: FilterRowType) => boolean; /** * Convert this model's constraints into `Expression[]` for the controller. * Each lexer filter becomes a `custom` condition with its dependency keys. * The negative-value rule also becomes a `custom` condition. */ private _modelConstraints; private _buildOptions; private _applyNegativePrefix; make(options?: OptionsType<PictFactorsType>): any[]; makeAsync(options?: OptionsType<PictFactorsType>): Generator<any, void, unknown>; } export declare class PictModelError extends Error { readonly issues: PictModelIssue[]; constructor(issues: PictModelIssue[]); } export declare interface PictModelIssue { severity: IssueSeverity; source: IssueSource; index: number; line: number; message: string; } export declare interface PictModelOptions { caseInsensitive?: boolean; strict?: boolean; } declare type PresetRowType = { [key: string]: any; [index: number]: any; }; declare class Row extends Map<ScalarType, number> implements RowType { /** Pair keys that failed constraint checks for this row attempt. */ invalidPairs: Set<ScalarType>; constructor(row: CandidateType); getPairKey(...newPair: number[]): ScalarType; copy(row: Row): void; } declare interface RowType { } declare type ScalarType = number | string; declare interface SortArgsType { salt: ScalarType; indices: IndicesType; } declare type SorterType = (pairs: PairType[], sortArgs: SortArgsType) => PairType[]; declare interface SubModelType { fields: ScalarType[]; strength: number; } declare type SuggestRowType<T extends FactorsType> = T extends ArrayTupleType ? T[number][number][] : T extends ArrayObjectType ? { [K in keyof T]: T[K][number]; } : unknown; declare interface UncoveredPair { /** The pair expressed as factor-key → value entries. */ pair: Record<string, any>; /** The constraint(s) that made this pair infeasible, if identifiable. */ constraints: number[]; } declare interface UncoveredPair_2 { /** The pair expressed as factor-key → value entries. */ pair: Record<string, any>; /** The constraint(s) that made this pair infeasible, if identifiable. */ constraints: number[]; } /** * Convert value-keyed weights to index-keyed weights for use with `OptionsType.weights`. * Useful when you want to specify weights by value rather than by index position. * * @example * weightsByValue( * { Browser: ["Chrome", "Firefox", "Safari"] }, * { Browser: { Chrome: 10, Safari: 5 } }, * ) * // → { Browser: { 0: 10, 2: 5 } } */ export declare function weightsByValue<T extends PictFactorsType>(factors: T, valueWeights: { [factorKey: string]: { [value: string]: number; }; }): { [factorKey: string]: { [index: number]: number; }; }; declare type WeightsType = { [factorKey: string]: { [index: number]: number; }; }; export { }