UNPKG

mod-engine

Version:

A TypeScript library for typed attributes and modifiers with deterministic evaluation

1,489 lines (1,476 loc) 42 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Builder: () => Builder, ConditionError: () => ConditionError, EngineBuilder: () => EngineBuilder, EvaluationError: () => EvaluationError, ModEngineError: () => ModEngineError, OperationError: () => OperationError, SchemaError: () => SchemaError, SerializationError: () => SerializationError, ValidationError: () => ValidationError, builtinOps: () => builtinOps, createBuiltInOperations: () => createBuiltInOperations, createConditionBuilder: () => createConditionBuilder, createEngine: () => createEngine, createEngineBuilder: () => createEngineBuilder, createMetricsSnapshot: () => createMetricsSnapshot, deepClone: () => deepClone, defineConfig: () => defineConfig, deserializeEvaluationResult: () => deserializeEvaluationResult, deserializeItem: () => deserializeItem, deserializeModifiers: () => deserializeModifiers, evaluateCondition: () => evaluateCondition, evaluateItem: () => evaluateItem, explainEvaluation: () => explainEvaluation, fromJSON: () => fromJSON, multiplyOperation: () => multiplyOperation, serializeEvaluationResult: () => serializeEvaluationResult, serializeItem: () => serializeItem, serializeModifiers: () => serializeModifiers, subtractOperation: () => subtractOperation, sumOperation: () => sumOperation, toJSON: () => toJSON, toSnapshot: () => toSnapshot, validateCondition: () => validateCondition, validateConfig: () => validateConfig, validateItem: () => validateItem, validateMetricsCompleteness: () => validateMetricsCompleteness, validateNumericResult: () => validateNumericResult, validateOperations: () => validateOperations }); module.exports = __toCommonJS(index_exports); // 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 }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { 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 });