covertable
Version:
Efficient TypeScript library for pairwise testing, generating minimal covering arrays with constraint support.
520 lines (476 loc) • 16.9 kB
TypeScript
/**
* 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 { }