mod-engine
Version:
A TypeScript library for typed attributes and modifiers with deterministic evaluation
1,254 lines (1,246 loc) • 34.1 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/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,
path
);
case "in":
return evaluateInclusion(
condition.attr,
condition.values,
attributes,
path
);
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, _path) {
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, _path) {
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/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 (error) {
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 (error) {
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}`);
}
}
// 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, config);
const sortedModifiers = sortModifiers(applicableModifiers, operations);
const applied = [];
for (const modifier of sortedModifiers) {
const application = applyModifier(
modifier,
metrics,
operations,
item,
config
);
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, _config) {
const validModifiers = item.modifiers.filter((modifier) => {
if (!modifier.conditions) {
return true;
}
try {
return evaluateCondition(modifier.conditions, item.attributes);
} catch (error) {
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 uniqueGroups.values()) {
if (group.length === 1) {
result.push(group[0]);
} else {
const best = group.reduce((best2, current, _index) => {
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, _config) {
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,
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 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/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;
/**
* Sets an attribute value with type safety
*/
set(key, value) {
this.attributes[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() {
this.currentCondition = void 0;
this.currentStacking = void 0;
this.currentPriority = void 0;
this.currentSource = void 0;
}
/**
* 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/config.ts
function defineConfig(config) {
validateConfig(config);
return config;
}
function createEngine(config) {
validateConfig(config);
const operations = createBuiltInOperations();
const engine = {
builder(name) {
return new Builder(config, name);
},
evaluate(item, options) {
return evaluateItem(item, operations, config, options?.base);
},
validateItem(item) {
return validateItem(item, config);
},
registerOperation(name, impl, options) {
operations.set(name, {
impl,
precedence: options?.precedence ?? 0
});
}
};
return engine;
}
export {
Builder,
ConditionError,
EvaluationError,
ModEngineError,
OperationError,
SchemaError,
SerializationError,
ValidationError,
createBuiltInOperations,
createConditionBuilder,
createEngine,
createMetricsSnapshot,
deepClone,
defineConfig,
deserializeEvaluationResult,
deserializeItem,
deserializeModifiers,
evaluateCondition,
evaluateItem,
fromJSON,
multiplyOperation,
serializeEvaluationResult,
serializeItem,
serializeModifiers,
subtractOperation,
sumOperation,
toJSON,
validateCondition,
validateConfig,
validateItem,
validateMetricsCompleteness,
validateNumericResult
};