mod-engine
Version:
A TypeScript library for typed attributes and modifiers with deterministic evaluation
1,425 lines (1,414 loc) • 39.3 kB
JavaScript
// src/errors.ts
var ModEngineError = class extends Error {
code;
path;
constructor(message, code, path) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.path = path;
if ("captureStackTrace" in Error && typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor);
}
}
};
var SchemaError = class extends ModEngineError {
constructor(message, path) {
super(message, "SCHEMA_ERROR", path);
}
};
var ValidationError = class extends ModEngineError {
constructor(message, path) {
super(message, "VALIDATION_ERROR", path);
}
};
var OperationError = class extends ModEngineError {
constructor(message, path) {
super(message, "OPERATION_ERROR", path);
}
};
var ConditionError = class extends ModEngineError {
constructor(message, path) {
super(message, "CONDITION_ERROR", path);
}
};
var EvaluationError = class extends ModEngineError {
constructor(message, path) {
super(message, "EVALUATION_ERROR", path);
}
};
var SerializationError = class extends ModEngineError {
constructor(message, path) {
super(message, "SERIALIZATION_ERROR", path);
}
};
// src/serialization.ts
var CURRENT_VERSION = 1;
function serializeItem(item) {
try {
const data = item.name !== void 0 ? {
name: item.name,
attributes: item.attributes,
modifiers: item.modifiers
} : {
attributes: item.attributes,
modifiers: item.modifiers
};
return {
version: CURRENT_VERSION,
data
};
} catch (error) {
throw new SerializationError(
`Failed to serialize item: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function deserializeItem(serialized) {
try {
if (!serialized || typeof serialized !== "object") {
throw new SerializationError("Invalid serialized data: not an object");
}
if (!("version" in serialized) || !("data" in serialized)) {
throw new SerializationError(
"Invalid serialized data: missing version or data"
);
}
if (serialized.version !== CURRENT_VERSION) {
throw new SerializationError(
`Unsupported version ${serialized.version}. Current version: ${CURRENT_VERSION}`
);
}
const data = serialized.data;
if (!data || typeof data !== "object") {
throw new SerializationError(
"Invalid serialized data: data is not an object"
);
}
if (!("attributes" in data) || !("modifiers" in data)) {
throw new SerializationError(
"Invalid serialized data: missing attributes or modifiers"
);
}
return data.name !== void 0 ? {
name: data.name,
attributes: data.attributes,
modifiers: data.modifiers
} : {
attributes: data.attributes,
modifiers: data.modifiers
};
} catch (error) {
if (error instanceof SerializationError) {
throw error;
}
throw new SerializationError(
`Failed to deserialize item: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function serializeModifiers(modifiers) {
try {
return {
version: CURRENT_VERSION,
data: modifiers
};
} catch (error) {
throw new SerializationError(
`Failed to serialize modifiers: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function deserializeModifiers(serialized) {
try {
if (!serialized || typeof serialized !== "object") {
throw new SerializationError("Invalid serialized data: not an object");
}
if (!("version" in serialized) || !("data" in serialized)) {
throw new SerializationError(
"Invalid serialized data: missing version or data"
);
}
if (serialized.version !== CURRENT_VERSION) {
throw new SerializationError(
`Unsupported version ${serialized.version}. Current version: ${CURRENT_VERSION}`
);
}
const data = serialized.data;
if (!Array.isArray(data)) {
throw new SerializationError(
"Invalid serialized data: data is not an array"
);
}
return data;
} catch (error) {
if (error instanceof SerializationError) {
throw error;
}
throw new SerializationError(
`Failed to deserialize modifiers: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function serializeEvaluationResult(result) {
try {
return {
version: CURRENT_VERSION,
data: {
metrics: result.metrics,
applied: result.applied
}
};
} catch (error) {
throw new SerializationError(
`Failed to serialize evaluation result: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function deserializeEvaluationResult(serialized) {
try {
if (!serialized || typeof serialized !== "object") {
throw new SerializationError("Invalid serialized data: not an object");
}
if (!("version" in serialized) || !("data" in serialized)) {
throw new SerializationError(
"Invalid serialized data: missing version or data"
);
}
if (serialized.version !== CURRENT_VERSION) {
throw new SerializationError(
`Unsupported version ${serialized.version}. Current version: ${CURRENT_VERSION}`
);
}
const data = serialized.data;
if (!data || typeof data !== "object") {
throw new SerializationError(
"Invalid serialized data: data is not an object"
);
}
if (!("metrics" in data) || !("applied" in data)) {
throw new SerializationError(
"Invalid serialized data: missing metrics or applied"
);
}
return {
metrics: data.metrics,
applied: data.applied
};
} catch (error) {
if (error instanceof SerializationError) {
throw error;
}
throw new SerializationError(
`Failed to deserialize evaluation result: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function toJSON(data) {
try {
return JSON.stringify(data, null, 2);
} catch (error) {
throw new SerializationError(
`Failed to convert to JSON: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function fromJSON(json) {
try {
return JSON.parse(json);
} catch (error) {
throw new SerializationError(
`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function deepClone(data) {
try {
return JSON.parse(JSON.stringify(data));
} catch (error) {
throw new SerializationError(
`Failed to deep clone: ${error instanceof Error ? error.message : String(error)}`
);
}
}
// src/builder.ts
var Builder = class _Builder {
constructor(config, name) {
this.config = config;
this.itemName = name;
}
itemName;
attributes = {};
modifiers = [];
currentCondition;
currentStacking;
currentPriority;
currentSource;
// Tracks nested group depth to manage context scoping
groupDepth = 0;
/**
* Sets an attribute value with type safety
*/
set(key, value) {
this.attributes[key] = value;
return this;
}
/**
* Applies multiple attributes from an object. Useful for defaults.
* Accepts a partial attributes map and merges into the current builder state.
*/
setAttributes(attrs) {
Object.entries(attrs).forEach(([key, value]) => {
this.set(key, value);
});
return this;
}
/**
* Sets a condition for subsequent modifiers
*/
when(condition) {
this.currentCondition = condition;
return this;
}
/**
* Sets metadata for subsequent modifiers
*/
with(metadata) {
if (metadata.stacking !== void 0) {
this.currentStacking = metadata.stacking;
}
if (metadata.priority !== void 0) {
this.currentPriority = metadata.priority;
}
if (metadata.source !== void 0) {
this.currentSource = metadata.source;
}
return this;
}
/**
* Adds an increase modifier (sum operation)
*/
increase(metric) {
return {
by: (value) => this.addModifier(metric, "sum", value)
};
}
/**
* Adds a decrease modifier (subtract operation)
*/
decrease(metric) {
return {
by: (value) => this.addModifier(metric, "subtract", value)
};
}
/**
* Adds a multiply modifier
*/
multiply(metric) {
return {
by: (value) => this.addModifier(metric, "multiply", value)
};
}
/**
* Adds a generic modifier with specified operation
*/
apply(metric, operation) {
return {
by: (value) => this.addModifier(metric, operation, value)
};
}
/**
* Internal method to add a modifier
*/
addModifier(metric, operation, value) {
if (!this.config.metrics.includes(metric)) {
throw new ValidationError(`Unknown metric: ${metric}`);
}
if (!this.config.operations.includes(operation)) {
throw new ValidationError(`Unknown operation: ${operation}`);
}
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new ValidationError(
`Modifier value must be a finite number, got: ${value}`
);
}
const modifier = {
metric,
operation,
value,
...this.currentCondition && { conditions: this.currentCondition },
...this.currentStacking && { stacking: this.currentStacking },
...this.currentPriority !== void 0 && {
priority: this.currentPriority
},
...this.currentSource && { source: this.currentSource }
};
this.modifiers.push(modifier);
this.resetCurrentContext();
return this;
}
/**
* Resets the current context (condition, stacking, etc.)
*/
resetCurrentContext() {
if (this.groupDepth > 0) {
return;
}
this.currentCondition = void 0;
this.currentStacking = void 0;
this.currentPriority = void 0;
this.currentSource = void 0;
}
/**
* Clears any pending condition so subsequent modifiers are unaffected.
*/
clearConditions() {
this.currentCondition = void 0;
return this;
}
/**
* Clears stacking / priority / source metadata applied to subsequent modifiers.
*/
clearMeta() {
this.currentStacking = void 0;
this.currentPriority = void 0;
this.currentSource = void 0;
return this;
}
/**
* Creates a temporary grouping context where the provided condition/meta apply
* to all modifiers executed within the callback. After the callback the
* previous context is restored automatically.
*/
group(options, fn) {
const prevCondition = this.currentCondition;
const prevStacking = this.currentStacking;
const prevPriority = this.currentPriority;
const prevSource = this.currentSource;
if (options.when) {
this.currentCondition = options.when;
}
if (options.with) {
if (options.with.stacking !== void 0)
this.currentStacking = options.with.stacking;
if (options.with.priority !== void 0)
this.currentPriority = options.with.priority;
if (options.with.source !== void 0)
this.currentSource = options.with.source;
}
this.groupDepth++;
try {
fn(this);
} finally {
this.groupDepth--;
this.currentCondition = prevCondition;
this.currentStacking = prevStacking;
this.currentPriority = prevPriority;
this.currentSource = prevSource;
}
return this;
}
/**
* Builds and returns the immutable item specification
*/
build() {
const result = {
attributes: this.attributes,
modifiers: this.modifiers
};
if (this.itemName !== void 0) {
result.name = this.itemName;
}
return result;
}
/**
* Creates a copy of the builder with the same state
*/
clone() {
const cloned = new _Builder(this.config, this.itemName);
cloned.attributes = deepClone(this.attributes);
cloned.modifiers = deepClone(this.modifiers);
cloned.currentCondition = this.currentCondition;
cloned.currentStacking = this.currentStacking;
cloned.currentPriority = this.currentPriority;
cloned.currentSource = this.currentSource;
return cloned;
}
/**
* Gets the current number of modifiers
*/
get modifierCount() {
return this.modifiers.length;
}
/**
* Gets the current number of attributes set
*/
get attributeCount() {
return Object.keys(this.attributes).length;
}
/**
* Clears all modifiers
*/
clearModifiers() {
this.modifiers = [];
this.resetCurrentContext();
return this;
}
/**
* Clears all attributes
*/
clearAttributes() {
this.attributes = {};
return this;
}
/**
* Resets the builder to initial state
*/
reset() {
this.attributes = {};
this.modifiers = [];
this.resetCurrentContext();
return this;
}
};
function createConditionBuilder() {
return {
eq: (attr, value) => ({
op: "eq",
attr,
value
}),
in: (attr, values) => ({
op: "in",
attr,
values
}),
includes: (attr, value) => ({
op: "includes",
attr,
value
}),
gt: (attr, value) => ({
op: "gt",
attr,
value
}),
gte: (attr, value) => ({
op: "gte",
attr,
value
}),
lt: (attr, value) => ({
op: "lt",
attr,
value
}),
lte: (attr, value) => ({
op: "lte",
attr,
value
}),
and: (...conditions) => ({
op: "and",
clauses: conditions
}),
or: (...conditions) => ({
op: "or",
clauses: conditions
}),
not: (condition) => ({
op: "not",
clause: condition
})
};
}
// src/conditions.ts
function evaluateCondition(condition, attributes, path = "condition") {
try {
switch (condition.op) {
case "and":
return condition.clauses.every(
(clause, index) => evaluateCondition(clause, attributes, `${path}.clauses[${index}]`)
);
case "or":
return condition.clauses.some(
(clause, index) => evaluateCondition(clause, attributes, `${path}.clauses[${index}]`)
);
case "not":
return !evaluateCondition(
condition.clause,
attributes,
`${path}.clause`
);
case "eq":
return evaluateEquality(condition.attr, condition.value, attributes);
case "in":
return evaluateInclusion(condition.attr, condition.values, attributes);
case "includes":
return evaluateContains(
condition.attr,
condition.value,
attributes,
path
);
case "gt":
return evaluateComparison(
condition.attr,
condition.value,
attributes,
path,
(a, b) => a > b
);
case "gte":
return evaluateComparison(
condition.attr,
condition.value,
attributes,
path,
(a, b) => a >= b
);
case "lt":
return evaluateComparison(
condition.attr,
condition.value,
attributes,
path,
(a, b) => a < b
);
case "lte":
return evaluateComparison(
condition.attr,
condition.value,
attributes,
path,
(a, b) => a <= b
);
default:
throw new ConditionError(
`Unknown condition operation: ${condition.op}`,
path
);
}
} catch (error) {
if (error instanceof ConditionError) {
throw error;
}
throw new ConditionError(
`Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`,
path
);
}
}
function evaluateEquality(attr, value, attributes) {
const attrValue = attributes[attr];
if (attrValue === void 0) {
return false;
}
if (Array.isArray(attrValue) && Array.isArray(value)) {
if (attrValue.length !== value.length) {
return false;
}
return attrValue.every((item, index) => item === value[index]);
}
return attrValue === value;
}
function evaluateInclusion(attr, values, attributes) {
const attrValue = attributes[attr];
if (attrValue === void 0) {
return false;
}
return values.includes(attrValue);
}
function evaluateContains(attr, value, attributes, path) {
const attrValue = attributes[attr];
if (attrValue === void 0) {
return false;
}
if (!Array.isArray(attrValue)) {
throw new ConditionError(
`Attribute '${attr}' is not an array for 'includes' operation`,
path
);
}
return attrValue.includes(value);
}
function evaluateComparison(attr, value, attributes, path, compareFn) {
const attrValue = attributes[attr];
if (attrValue === void 0) {
return false;
}
if (typeof attrValue !== "number") {
throw new ConditionError(
`Attribute '${attr}' is not a number for comparison operation`,
path
);
}
return compareFn(attrValue, value);
}
function validateCondition(condition, config, path = "condition") {
try {
switch (condition.op) {
case "and":
case "or":
if (!Array.isArray(condition.clauses) || condition.clauses.length === 0) {
throw new ConditionError(
`${condition.op} condition must have non-empty clauses array`,
path
);
}
condition.clauses.forEach(
(clause, index) => validateCondition(clause, config, `${path}.clauses[${index}]`)
);
break;
case "not":
validateCondition(condition.clause, config, `${path}.clause`);
break;
case "eq":
case "includes":
validateAttributeReference(condition.attr, config, path);
break;
case "in":
validateAttributeReference(condition.attr, config, path);
if (!Array.isArray(condition.values) || condition.values.length === 0) {
throw new ConditionError(
`'in' condition must have non-empty values array`,
path
);
}
break;
case "gt":
case "gte":
case "lt":
case "lte":
validateAttributeReference(condition.attr, config, path);
if (typeof condition.value !== "number") {
throw new ConditionError(
`Comparison conditions require numeric value`,
path
);
}
break;
default:
throw new ConditionError(
`Unknown condition operation: ${condition.op}`,
path
);
}
} catch (error) {
if (error instanceof ConditionError) {
throw error;
}
throw new ConditionError(
`Condition validation failed: ${error instanceof Error ? error.message : String(error)}`,
path
);
}
}
function validateAttributeReference(attr, config, path) {
const validAttrs = config.attributes.map((a) => a.key);
if (!validAttrs.includes(attr)) {
throw new ConditionError(
`Unknown attribute '${attr}'. Valid attributes: ${validAttrs.join(", ")}`,
path
);
}
}
// src/evaluation.ts
function evaluateItem(item, operations, config, baseMetrics) {
try {
const metrics = {};
for (const metric of config.metrics) {
metrics[metric] = baseMetrics?.[metric] ?? 0;
}
const applicableModifiers = filterAndStackModifiers(item);
const sortedModifiers = sortModifiers(applicableModifiers, operations);
const applied = [];
for (const modifier of sortedModifiers) {
const application = applyModifier(modifier, metrics, operations, item);
if (application) {
applied.push(application);
}
}
return {
metrics,
applied
};
} catch (error) {
throw new EvaluationError(
`Evaluation failed: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function filterAndStackModifiers(item) {
const validModifiers = item.modifiers.filter((modifier) => {
if (!modifier.conditions) {
return true;
}
try {
return evaluateCondition(modifier.conditions, item.attributes);
} catch {
return false;
}
});
return applyStackingRules(validModifiers);
}
function applyStackingRules(modifiers) {
const result = [];
const uniqueGroups = /* @__PURE__ */ new Map();
for (const modifier of modifiers) {
const stacking = modifier.stacking || "stack";
if (stacking === "stack") {
result.push(modifier);
} else if (stacking === "unique") {
const source = modifier.source || "";
const groupKey = `${modifier.metric}:${modifier.operation}:${source}`;
if (!uniqueGroups.has(groupKey)) {
uniqueGroups.set(groupKey, []);
}
uniqueGroups.get(groupKey).push(modifier);
} else if (typeof stacking === "object" && "uniqueBy" in stacking) {
const groupKey = stacking.uniqueBy;
if (!uniqueGroups.has(groupKey)) {
uniqueGroups.set(groupKey, []);
}
uniqueGroups.get(groupKey).push(modifier);
}
}
for (const group of Array.from(uniqueGroups.values())) {
if (group.length === 1) {
result.push(group[0]);
} else {
const best = group.reduce((best2, current) => {
const bestAbsValue = Math.abs(best2.value);
const currentAbsValue = Math.abs(current.value);
if (currentAbsValue > bestAbsValue) {
return current;
} else if (currentAbsValue === bestAbsValue) {
const bestPriority = best2.priority ?? 0;
const currentPriority = current.priority ?? 0;
if (currentPriority > bestPriority) {
return current;
} else if (currentPriority === bestPriority) {
return best2;
}
}
return best2;
});
result.push(best);
}
}
return result;
}
function sortModifiers(modifiers, operations) {
return [...modifiers].sort((a, b) => {
const aPriority = a.priority ?? 0;
const bPriority = b.priority ?? 0;
if (aPriority !== bPriority) {
return bPriority - aPriority;
}
const aOp = operations.get(a.operation);
const bOp = operations.get(b.operation);
const aPrecedence = aOp?.precedence ?? 0;
const bPrecedence = bOp?.precedence ?? 0;
if (aPrecedence !== bPrecedence) {
return bPrecedence - aPrecedence;
}
return 0;
});
}
function applyModifier(modifier, metrics, operations, item) {
const operationInfo = operations.get(modifier.operation);
if (!operationInfo) {
throw new EvaluationError(`Unknown operation: ${modifier.operation}`);
}
const currentValue = metrics[modifier.metric];
const context = {
item,
modifier,
currentMetrics: { ...metrics }
};
try {
const newValue = operationInfo.impl(currentValue, modifier.value, context);
if (!Number.isFinite(newValue)) {
throw new EvaluationError(
`Operation ${modifier.operation} produced invalid result: ${newValue}`
);
}
metrics[modifier.metric] = newValue;
return {
modifier,
appliedValue: modifier.value,
before: currentValue,
after: newValue,
resultingValue: newValue
};
} catch (error) {
throw new EvaluationError(
`Failed to apply modifier ${modifier.operation} to ${modifier.metric}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
function createMetricsSnapshot(metrics) {
return { ...metrics };
}
function validateMetricsCompleteness(metrics, config) {
for (const metric of config.metrics) {
if (!(metric in metrics)) {
throw new EvaluationError(`Missing metric in result: ${metric}`);
}
}
}
// src/operations.ts
function builtinOps(...operations) {
return operations;
}
function sumOperation() {
return (current, value) => current + value;
}
function subtractOperation() {
return (current, value) => current - value;
}
function multiplyOperation() {
return (current, value) => current * value;
}
function createBuiltInOperations() {
const operations = /* @__PURE__ */ new Map();
operations.set("sum", {
impl: sumOperation(),
precedence: 10
});
operations.set("subtract", {
impl: subtractOperation(),
precedence: 10
});
operations.set("multiply", {
impl: multiplyOperation(),
precedence: 20
});
return operations;
}
function validateNumericResult(value, path) {
if (!Number.isFinite(value)) {
throw new Error(`Invalid numeric result at ${path}: ${value}`);
}
return value;
}
// src/validation.ts
function validateItem(item, config) {
const errors = [];
try {
validateAttributes(item.attributes, config, errors);
validateModifiers(item.modifiers, config, errors);
} catch (error) {
if (error instanceof ValidationError) {
errors.push({
path: error.path || "item",
message: error.message,
code: error.code
});
} else {
errors.push({
path: "item",
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`,
code: "UNKNOWN_ERROR"
});
}
}
if (errors.length === 0) {
return { ok: true };
}
return {
ok: false,
errors
};
}
function validateAttributes(attributes, config, errors) {
const validAttrKeys = new Set(config.attributes.map((attr) => attr.key));
for (const key of Object.keys(attributes)) {
if (!validAttrKeys.has(key)) {
errors.push({
path: `attributes.${key}`,
message: `Unknown attribute '${key}'. Valid attributes: ${Array.from(
validAttrKeys
).join(", ")}`,
code: "UNKNOWN_ATTRIBUTE"
});
}
}
for (const attrSchema of config.attributes) {
const value = attributes[attrSchema.key];
if (value !== void 0) {
validateAttributeValue(
value,
attrSchema,
`attributes.${attrSchema.key}`,
errors
);
}
}
}
function validateAttributeValue(value, schema, path, errors) {
try {
switch (schema.kind) {
case "enum":
validateEnumValue(value, schema, path, errors);
break;
case "boolean":
if (typeof value !== "boolean") {
errors.push({
path,
message: `Expected boolean, got ${typeof value}`,
code: "INVALID_TYPE"
});
}
break;
case "number":
validateNumberValue(value, schema, path, errors);
break;
case "string":
validateStringValue(value, schema, path, errors);
break;
default:
errors.push({
path,
message: `Unknown attribute kind: ${schema.kind}`,
code: "UNKNOWN_ATTRIBUTE_KIND"
});
}
} catch (error) {
errors.push({
path,
message: `Attribute validation failed: ${error instanceof Error ? error.message : String(error)}`,
code: "VALIDATION_ERROR"
});
}
}
function validateEnumValue(value, schema, path, errors) {
const cardinality = schema.cardinality || "single";
if (cardinality === "single") {
if (Array.isArray(value)) {
errors.push({
path,
message: `Single-select enum cannot be an array`,
code: "INVALID_CARDINALITY"
});
return;
}
if (typeof value !== "string" || !schema.values.includes(value)) {
errors.push({
path,
message: `Invalid enum value '${value}'. Valid values: ${schema.values.join(
", "
)}`,
code: "INVALID_ENUM_VALUE"
});
}
} else if (cardinality === "multi") {
if (!Array.isArray(value)) {
errors.push({
path,
message: `Multi-select enum must be an array`,
code: "INVALID_CARDINALITY"
});
return;
}
const invalidValues = value.filter((v) => !schema.values.includes(v));
if (invalidValues.length > 0) {
errors.push({
path,
message: `Invalid enum values: ${invalidValues.join(
", "
)}. Valid values: ${schema.values.join(", ")}`,
code: "INVALID_ENUM_VALUE"
});
}
const uniqueValues = new Set(value);
if (uniqueValues.size !== value.length) {
errors.push({
path,
message: `Multi-select enum contains duplicate values`,
code: "DUPLICATE_VALUES"
});
}
}
}
function validateNumberValue(value, schema, path, errors) {
if (typeof value !== "number") {
errors.push({
path,
message: `Expected number, got ${typeof value}`,
code: "INVALID_TYPE"
});
return;
}
if (!Number.isFinite(value)) {
errors.push({
path,
message: `Number must be finite, got ${value}`,
code: "INVALID_NUMBER"
});
return;
}
if (schema.integer === true && !Number.isInteger(value)) {
errors.push({
path,
message: `Expected integer, got ${value}`,
code: "INVALID_INTEGER"
});
}
if (schema.min !== void 0 && value < schema.min) {
errors.push({
path,
message: `Value ${value} is below minimum ${schema.min}`,
code: "VALUE_TOO_LOW"
});
}
if (schema.max !== void 0 && value > schema.max) {
errors.push({
path,
message: `Value ${value} is above maximum ${schema.max}`,
code: "VALUE_TOO_HIGH"
});
}
}
function validateStringValue(value, schema, path, errors) {
if (typeof value !== "string") {
errors.push({
path,
message: `Expected string, got ${typeof value}`,
code: "INVALID_TYPE"
});
return;
}
if (schema.minLen !== void 0 && value.length < schema.minLen) {
errors.push({
path,
message: `String length ${value.length} is below minimum ${schema.minLen}`,
code: "STRING_TOO_SHORT"
});
}
if (schema.maxLen !== void 0 && value.length > schema.maxLen) {
errors.push({
path,
message: `String length ${value.length} is above maximum ${schema.maxLen}`,
code: "STRING_TOO_LONG"
});
}
if (schema.pattern !== void 0) {
try {
const regex = new RegExp(schema.pattern);
if (!regex.test(value)) {
errors.push({
path,
message: `String '${value}' does not match pattern ${schema.pattern}`,
code: "PATTERN_MISMATCH"
});
}
} catch {
errors.push({
path,
message: `Invalid regex pattern: ${schema.pattern}`,
code: "INVALID_PATTERN"
});
}
}
}
function validateModifiers(modifiers, config, errors) {
modifiers.forEach((modifier, index) => {
validateModifier(modifier, config, `modifiers[${index}]`, errors);
});
}
function validateModifier(modifier, config, path, errors) {
if (!config.metrics.includes(modifier.metric)) {
errors.push({
path: `${path}.metric`,
message: `Unknown metric '${modifier.metric}'. Valid metrics: ${config.metrics.join(", ")}`,
code: "UNKNOWN_METRIC"
});
}
if (!config.operations.includes(modifier.operation)) {
errors.push({
path: `${path}.operation`,
message: `Unknown operation '${modifier.operation}'. Valid operations: ${config.operations.join(", ")}`,
code: "UNKNOWN_OPERATION"
});
}
if (typeof modifier.value !== "number" || !Number.isFinite(modifier.value)) {
errors.push({
path: `${path}.value`,
message: `Modifier value must be a finite number, got ${modifier.value}`,
code: "INVALID_MODIFIER_VALUE"
});
}
if (modifier.priority !== void 0 && (typeof modifier.priority !== "number" || !Number.isInteger(modifier.priority))) {
errors.push({
path: `${path}.priority`,
message: `Priority must be an integer, got ${modifier.priority}`,
code: "INVALID_PRIORITY"
});
}
if (modifier.conditions !== void 0) {
try {
validateCondition(modifier.conditions, config, `${path}.conditions`);
} catch (error) {
errors.push({
path: `${path}.conditions`,
message: error instanceof Error ? error.message : String(error),
code: "INVALID_CONDITION"
});
}
}
}
function validateConfig(config) {
if (!Array.isArray(config.metrics) || config.metrics.length === 0) {
throw new SchemaError("Config must have at least one metric");
}
const uniqueMetrics = new Set(config.metrics);
if (uniqueMetrics.size !== config.metrics.length) {
throw new SchemaError("Metric names must be unique");
}
if (!Array.isArray(config.operations) || config.operations.length === 0) {
throw new SchemaError("Config must have at least one operation");
}
const uniqueOperations = new Set(config.operations);
if (uniqueOperations.size !== config.operations.length) {
throw new SchemaError("Operation names must be unique");
}
if (!Array.isArray(config.attributes)) {
throw new SchemaError("Config attributes must be an array");
}
const attrKeys = /* @__PURE__ */ new Set();
for (const attr of config.attributes) {
if (!attr.key || typeof attr.key !== "string") {
throw new SchemaError("Attribute must have a valid string key");
}
if (attrKeys.has(attr.key)) {
throw new SchemaError(`Duplicate attribute key: ${attr.key}`);
}
attrKeys.add(attr.key);
validateAttributeSchema(attr);
}
}
function validateAttributeSchema(schema) {
switch (schema.kind) {
case "enum": {
if (!Array.isArray(schema.values) || schema.values.length === 0) {
throw new SchemaError(
`Enum attribute '${schema.key}' must have non-empty values array`
);
}
const uniqueValues = new Set(schema.values);
if (uniqueValues.size !== schema.values.length) {
throw new SchemaError(
`Enum attribute '${schema.key}' has duplicate values`
);
}
if (schema.cardinality && !["single", "multi"].includes(schema.cardinality)) {
throw new SchemaError(
`Enum attribute '${schema.key}' has invalid cardinality: ${schema.cardinality}`
);
}
break;
}
case "number":
if (schema.min !== void 0 && schema.max !== void 0 && schema.min > schema.max) {
throw new SchemaError(`Number attribute '${schema.key}' has min > max`);
}
break;
case "string":
if (schema.minLen !== void 0 && schema.maxLen !== void 0 && schema.minLen > schema.maxLen) {
throw new SchemaError(
`String attribute '${schema.key}' has minLen > maxLen`
);
}
if (schema.pattern !== void 0) {
try {
new RegExp(schema.pattern);
} catch {
throw new SchemaError(
`String attribute '${schema.key}' has invalid regex pattern: ${schema.pattern}`
);
}
}
break;
case "boolean":
break;
default:
throw new SchemaError(
`Unknown attribute kind: ${schema.kind}`
);
}
}
function validateOperations(config, operations) {
const builtinOps2 = /* @__PURE__ */ new Set(["sum", "subtract", "multiply"]);
const missingOperations = [];
for (const opName of config.operations) {
if (!builtinOps2.has(opName) && !operations.has(opName)) {
missingOperations.push(opName);
}
}
if (missingOperations.length > 0) {
const operationList = missingOperations.map((op) => `"${op}"`).join(", ");
throw new ValidationError(
`Custom operations declared but not registered: ${operationList}. Use engine.registerOperation() to register these operations before using the engine.`,
"MISSING_OPERATIONS"
);
}
}
// src/config.ts
function defineConfig(config) {
validateConfig(config);
return config;
}
function createEngine(config, options) {
validateConfig(config);
const operations = createBuiltInOperations();
if (options?.strictOperations !== false) {
validateOperations(config, operations);
}
const engine = {
builder(name) {
return new Builder(config, name);
},
evaluate(item, options2) {
return evaluateItem(item, operations, config, options2?.base);
},
validateItem(item) {
return validateItem(item, config);
},
registerOperation(name, impl, options2) {
operations.set(name, {
impl,
precedence: options2?.precedence ?? 0
});
}
};
return engine;
}
// src/engine-builder.ts
var EngineBuilder = class {
constructor(config) {
this.config = config;
validateConfig(config);
}
operations = createBuiltInOperations();
builtinOps = /* @__PURE__ */ new Set(["sum", "subtract", "multiply"]);
/**
* Register a custom operation with its implementation
*/
withOperation(name, impl, options) {
this.operations.set(name, {
impl,
precedence: options?.precedence ?? 0
});
return this;
}
/**
* Register multiple operations at once
*/
withOperations(operations) {
for (const [name, opInfo] of Object.entries(operations)) {
const { impl, precedence } = opInfo;
this.operations.set(name, {
impl,
precedence: precedence ?? 0
});
}
return this;
}
/**
* Build the engine - validates that all custom operations are registered
*/
build() {
const missingOperations = [];
for (const opName of this.config.operations) {
if (!this.builtinOps.has(opName) && !this.operations.has(opName)) {
missingOperations.push(opName);
}
}
if (missingOperations.length > 0) {
const operationList = missingOperations.map((op) => `"${op}"`).join(", ");
throw new Error(
`Cannot build engine: Custom operations ${operationList} must be registered. Use .withOperation() or .withOperations() to register them.`
);
}
const engine = createEngine(this.config, { strictOperations: false });
for (const [name, info] of this.operations) {
if (!this.builtinOps.has(name)) {
engine.registerOperation?.(name, info.impl, {
precedence: info.precedence
});
}
}
return engine;
}
};
function createEngineBuilder(config) {
return new EngineBuilder(config);
}
// src/explain.ts
function explainEvaluation(result) {
return result.applied.map((app) => {
const { modifier, before, after } = app;
return {
metric: modifier.metric,
operation: modifier.operation,
value: modifier.value,
before,
after,
priority: modifier.priority,
source: modifier.source,
conditionMatched: true
// Only matched modifiers are included in EvaluationResult
};
});
}
// src/snapshot.ts
function toSnapshot(engine, item, baseStats) {
const evaluation = engine.evaluate(item, { base: baseStats });
return {
name: item.name || "Unknown",
metrics: evaluation.metrics,
...item.attributes
};
}
export {
Builder,
ConditionError,
EngineBuilder,
EvaluationError,
ModEngineError,
OperationError,
SchemaError,
SerializationError,
ValidationError,
builtinOps,
createBuiltInOperations,
createConditionBuilder,
createEngine,
createEngineBuilder,
createMetricsSnapshot,
deepClone,
defineConfig,
deserializeEvaluationResult,
deserializeItem,
deserializeModifiers,
evaluateCondition,
evaluateItem,
explainEvaluation,
fromJSON,
multiplyOperation,
serializeEvaluationResult,
serializeItem,
serializeModifiers,
subtractOperation,
sumOperation,
toJSON,
toSnapshot,
validateCondition,
validateConfig,
validateItem,
validateMetricsCompleteness,
validateNumericResult,
validateOperations
};