@run-iq/plugin-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Abdou-Raouf ATARMLA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @run-iq/plugin-sdk
2
+
3
+ SDK for building PPE plugins — provides base classes, parameter validation, and compliance testing utilities.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @run-iq/plugin-sdk
9
+ ```
10
+
11
+ **Peer dependency:** `@run-iq/core >= 0.1.0`
12
+
13
+ ## Building a plugin
14
+
15
+ Extend `BasePlugin` and declare your calculation models:
16
+
17
+ ```typescript
18
+ import { BasePlugin, BaseModel, SchemaValidator } from '@run-iq/plugin-sdk';
19
+ import type { Rule, ValidationResult } from '@run-iq/plugin-sdk';
20
+
21
+ class MyModel extends BaseModel {
22
+ readonly name = 'MY_MODEL';
23
+ readonly version = '1.0.0';
24
+
25
+ validateParams(params: unknown): ValidationResult {
26
+ return SchemaValidator.validate(params, {
27
+ rate: { type: 'number', min: 0, max: 1 },
28
+ base: { type: 'string' },
29
+ });
30
+ }
31
+
32
+ calculate(input: Record<string, unknown>, _rule: Readonly<Rule>, params: unknown): number {
33
+ const { rate, base } = params as { rate: number; base: string };
34
+ return (input[base] as number) * rate;
35
+ }
36
+ }
37
+
38
+ export class MyPlugin extends BasePlugin {
39
+ readonly name = 'my-plugin';
40
+ readonly version = '1.0.0';
41
+ readonly models = [new MyModel()];
42
+ }
43
+ ```
44
+
45
+ `BasePlugin.onInit()` automatically registers all declared models into the engine's `ModelRegistry`.
46
+
47
+ ## Schema validation
48
+
49
+ `SchemaValidator` validates `params` against a declarative schema:
50
+
51
+ ```typescript
52
+ import { SchemaValidator } from '@run-iq/plugin-sdk';
53
+
54
+ const result = SchemaValidator.validate(params, {
55
+ rate: { type: 'number', min: 0, max: 1 },
56
+ amount: { type: 'number', min: 0 },
57
+ label: { type: 'string' },
58
+ });
59
+
60
+ if (!result.valid) {
61
+ console.log(result.errors);
62
+ }
63
+ ```
64
+
65
+ ## Compliance testing
66
+
67
+ `PluginTester` verifies that a plugin respects PPE invariants:
68
+
69
+ ```typescript
70
+ import { PluginTester } from '@run-iq/plugin-sdk';
71
+
72
+ const tester = new PluginTester(myPlugin);
73
+ const report = await tester.runAll(input, rules, context);
74
+
75
+ console.log(report.summary); // 'PASS' or 'FAIL'
76
+ ```
77
+
78
+ Individual assertions:
79
+
80
+ | Method | Checks |
81
+ |---|---|
82
+ | `assertDeterminism()` | Same input x3 produces identical output |
83
+ | `assertImmutability()` | Hooks never mutate received data |
84
+ | `assertNoSideEffects()` | Consecutive calls produce identical results |
85
+ | `assertParamsValidation()` | Model rejects invalid params |
86
+
87
+ ## Exports
88
+
89
+ | Export | Type | Description |
90
+ |---|---|---|
91
+ | `BasePlugin` | class | Abstract plugin base with auto model registration |
92
+ | `BaseModel` | class | Abstract model base with params validation guard |
93
+ | `PluginTester` | class | Compliance test runner |
94
+ | `SchemaValidator` | class | Declarative parameter validation |
95
+ | All `@run-iq/core` types | re-export | Convenience re-exports |
96
+
97
+ ## Requirements
98
+
99
+ - Node.js >= 20
100
+ - `@run-iq/core` >= 0.1.0
101
+
102
+ ## License
103
+
104
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BaseModel: () => BaseModel,
24
+ BasePlugin: () => BasePlugin,
25
+ PluginTester: () => PluginTester,
26
+ SchemaValidator: () => SchemaValidator
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/base/BasePlugin.ts
31
+ var BasePlugin = class {
32
+ onInit(context) {
33
+ for (const model of this.models) {
34
+ context.modelRegistry.register(model);
35
+ }
36
+ }
37
+ beforeEvaluate(input, _rules) {
38
+ return input;
39
+ }
40
+ afterEvaluate(_input, result) {
41
+ return result;
42
+ }
43
+ onError(_error, _input) {
44
+ }
45
+ teardown() {
46
+ }
47
+ };
48
+
49
+ // src/base/BaseModel.ts
50
+ var BaseModel = class {
51
+ };
52
+
53
+ // src/testing/PluginTester.ts
54
+ var PluginTester = class {
55
+ plugin;
56
+ constructor(plugin) {
57
+ this.plugin = plugin;
58
+ }
59
+ async assertDeterminism(input, rules, context) {
60
+ this.plugin.onInit(context);
61
+ const results = [];
62
+ for (let i = 0; i < 3; i++) {
63
+ if (this.plugin.beforeEvaluate) {
64
+ results.push(this.plugin.beforeEvaluate(input, rules));
65
+ }
66
+ }
67
+ if (results.length === 3) {
68
+ const json0 = JSON.stringify(results[0]);
69
+ const json1 = JSON.stringify(results[1]);
70
+ const json2 = JSON.stringify(results[2]);
71
+ if (json0 !== json1 || json1 !== json2) {
72
+ throw new Error("Determinism violation: different results for same input");
73
+ }
74
+ }
75
+ }
76
+ async assertImmutability(input, rules) {
77
+ const inputCopy = JSON.parse(JSON.stringify(input));
78
+ const rulesCopy = JSON.parse(JSON.stringify(rules));
79
+ if (this.plugin.beforeEvaluate) {
80
+ this.plugin.beforeEvaluate(input, rules);
81
+ }
82
+ if (JSON.stringify(input) !== JSON.stringify(inputCopy)) {
83
+ throw new Error("Immutability violation: beforeEvaluate mutated input");
84
+ }
85
+ if (JSON.stringify(rules) !== JSON.stringify(rulesCopy)) {
86
+ throw new Error("Immutability violation: beforeEvaluate mutated rules");
87
+ }
88
+ }
89
+ async assertParamsValidation(modelName, invalidParams, context) {
90
+ this.plugin.onInit(context);
91
+ const model = this.findModel(modelName, context);
92
+ if (!model) {
93
+ throw new Error(`Model "${modelName}" not found in plugin`);
94
+ }
95
+ for (const params of invalidParams) {
96
+ const result = model.validateParams(params);
97
+ if (result.valid) {
98
+ throw new Error(
99
+ `Expected model "${modelName}" to reject params: ${JSON.stringify(params)}`
100
+ );
101
+ }
102
+ }
103
+ }
104
+ async assertNoSideEffects(input, rules) {
105
+ if (this.plugin.beforeEvaluate) {
106
+ const r1 = this.plugin.beforeEvaluate(input, rules);
107
+ const r2 = this.plugin.beforeEvaluate(input, rules);
108
+ if (JSON.stringify(r1) !== JSON.stringify(r2)) {
109
+ throw new Error("Side effects detected: different results between calls");
110
+ }
111
+ }
112
+ }
113
+ async runAll(input, rules, context) {
114
+ const passed = [];
115
+ const failed = [];
116
+ const tests = [
117
+ { name: "determinism", fn: () => this.assertDeterminism(input, rules, context) },
118
+ { name: "immutability", fn: () => this.assertImmutability(input, rules) },
119
+ { name: "noSideEffects", fn: () => this.assertNoSideEffects(input, rules) }
120
+ ];
121
+ for (const t of tests) {
122
+ try {
123
+ await t.fn();
124
+ passed.push(t.name);
125
+ } catch (error) {
126
+ failed.push({
127
+ test: t.name,
128
+ error: error instanceof Error ? error.message : String(error)
129
+ });
130
+ }
131
+ }
132
+ return {
133
+ passed,
134
+ failed,
135
+ summary: failed.length === 0 ? "PASS" : "FAIL"
136
+ };
137
+ }
138
+ findModel(name, context) {
139
+ try {
140
+ return context.modelRegistry.get(name);
141
+ } catch {
142
+ return void 0;
143
+ }
144
+ }
145
+ };
146
+
147
+ // src/validation/SchemaValidator.ts
148
+ var SchemaValidator = class _SchemaValidator {
149
+ static validate(params, schema) {
150
+ const errors = [];
151
+ if (params === null || typeof params !== "object") {
152
+ return { valid: false, errors: ["params must be a non-null object"] };
153
+ }
154
+ const obj = params;
155
+ for (const [key, def] of Object.entries(schema)) {
156
+ const value = obj[key];
157
+ const isRequired = def.required !== false;
158
+ if (value === void 0 || value === null) {
159
+ if (isRequired) {
160
+ errors.push(`"${key}" is required`);
161
+ }
162
+ continue;
163
+ }
164
+ if (def.type === "number") {
165
+ if (typeof value !== "number" || Number.isNaN(value)) {
166
+ errors.push(`"${key}" must be a number`);
167
+ continue;
168
+ }
169
+ if (def.min !== void 0 && value < def.min) {
170
+ errors.push(`"${key}" must be >= ${def.min}`);
171
+ }
172
+ if (def.max !== void 0 && value > def.max) {
173
+ errors.push(`"${key}" must be <= ${def.max}`);
174
+ }
175
+ } else if (def.type === "string") {
176
+ if (typeof value !== "string") {
177
+ errors.push(`"${key}" must be a string`);
178
+ continue;
179
+ }
180
+ if (def.enum && !def.enum.includes(value)) {
181
+ errors.push(`"${key}" must be one of: ${def.enum.join(", ")}`);
182
+ }
183
+ } else if (def.type === "boolean") {
184
+ if (typeof value !== "boolean") {
185
+ errors.push(`"${key}" must be a boolean`);
186
+ }
187
+ } else if (def.type === "array") {
188
+ if (!Array.isArray(value)) {
189
+ errors.push(`"${key}" must be an array`);
190
+ }
191
+ } else if (def.type === "object") {
192
+ if (typeof value !== "object" || Array.isArray(value)) {
193
+ errors.push(`"${key}" must be an object`);
194
+ }
195
+ }
196
+ }
197
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
198
+ }
199
+ static number(value, opts) {
200
+ if (typeof value !== "number" || Number.isNaN(value)) return false;
201
+ if (opts?.min !== void 0 && value < opts.min) return false;
202
+ if (opts?.max !== void 0 && value > opts.max) return false;
203
+ return true;
204
+ }
205
+ static string(value, opts) {
206
+ if (typeof value !== "string") return false;
207
+ if (opts?.enum && !opts.enum.includes(value)) return false;
208
+ return true;
209
+ }
210
+ static positiveNumber(value) {
211
+ return _SchemaValidator.number(value, { min: 0 });
212
+ }
213
+ static rate(value) {
214
+ return _SchemaValidator.number(value, { min: 0, max: 1 });
215
+ }
216
+ };
217
+ // Annotate the CommonJS export names for ESM import in node:
218
+ 0 && (module.exports = {
219
+ BaseModel,
220
+ BasePlugin,
221
+ PluginTester,
222
+ SchemaValidator
223
+ });
224
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/base/BasePlugin.ts","../src/base/BaseModel.ts","../src/testing/PluginTester.ts","../src/validation/SchemaValidator.ts"],"sourcesContent":["export { BasePlugin } from './base/BasePlugin.js';\nexport { BaseModel } from './base/BaseModel.js';\nexport { PluginTester } from './testing/PluginTester.js';\nexport type { TestReport } from './testing/PluginTester.js';\nexport { SchemaValidator } from './validation/SchemaValidator.js';\nexport type { SchemaDefinition, SchemaFieldDef } from './validation/SchemaValidator.js';\nexport type {\n Rule,\n Expression,\n EvaluationInput,\n EvaluationResult,\n BreakdownItem,\n SkippedRule,\n SkipReason,\n PPEPlugin,\n PluginContext,\n CalculationModel,\n ValidationResult,\n DSLEvaluator,\n ISnapshotAdapter,\n Snapshot,\n EvaluationTrace,\n TraceStep,\n} from './types/index.js';\n","import type {\n PPEPlugin,\n PluginContext,\n EvaluationInput,\n EvaluationResult,\n Rule,\n CalculationModel,\n PPEError,\n} from '@run-iq/core';\n\nexport abstract class BasePlugin implements PPEPlugin {\n abstract readonly name: string;\n abstract readonly version: string;\n abstract readonly models: CalculationModel[];\n\n onInit(context: PluginContext): void {\n for (const model of this.models) {\n context.modelRegistry.register(model);\n }\n }\n\n beforeEvaluate(input: EvaluationInput, _rules: ReadonlyArray<Rule>): EvaluationInput {\n return input;\n }\n\n afterEvaluate(_input: EvaluationInput, result: EvaluationResult): EvaluationResult {\n return result;\n }\n\n onError(_error: PPEError, _input: EvaluationInput): void {\n // no-op by default\n }\n\n teardown(): void {\n // no-op by default\n }\n}\n","import type { CalculationModel, ValidationResult, Rule } from '@run-iq/core';\n\nexport abstract class BaseModel implements CalculationModel {\n abstract readonly name: string;\n abstract readonly version: string;\n\n abstract validateParams(params: unknown): ValidationResult;\n\n abstract calculate(\n input: Record<string, unknown>,\n matchedRule: Readonly<Rule>,\n params: unknown,\n ): number;\n}\n","import type {\n PPEPlugin,\n PluginContext,\n EvaluationInput,\n Rule,\n CalculationModel,\n} from '@run-iq/core';\n\nexport interface TestReport {\n readonly passed: string[];\n readonly failed: Array<{ test: string; error: string }>;\n readonly summary: 'PASS' | 'FAIL';\n}\n\nexport class PluginTester {\n private readonly plugin: PPEPlugin;\n\n constructor(plugin: PPEPlugin) {\n this.plugin = plugin;\n }\n\n async assertDeterminism(\n input: EvaluationInput,\n rules: ReadonlyArray<Rule>,\n context: PluginContext,\n ): Promise<void> {\n this.plugin.onInit(context);\n\n const results: EvaluationInput[] = [];\n for (let i = 0; i < 3; i++) {\n if (this.plugin.beforeEvaluate) {\n results.push(this.plugin.beforeEvaluate(input, rules));\n }\n }\n\n if (results.length === 3) {\n const json0 = JSON.stringify(results[0]);\n const json1 = JSON.stringify(results[1]);\n const json2 = JSON.stringify(results[2]);\n if (json0 !== json1 || json1 !== json2) {\n throw new Error('Determinism violation: different results for same input');\n }\n }\n }\n\n async assertImmutability(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void> {\n const inputCopy = JSON.parse(JSON.stringify(input)) as EvaluationInput;\n const rulesCopy = JSON.parse(JSON.stringify(rules)) as Rule[];\n\n if (this.plugin.beforeEvaluate) {\n this.plugin.beforeEvaluate(input, rules);\n }\n\n if (JSON.stringify(input) !== JSON.stringify(inputCopy)) {\n throw new Error('Immutability violation: beforeEvaluate mutated input');\n }\n if (JSON.stringify(rules) !== JSON.stringify(rulesCopy)) {\n throw new Error('Immutability violation: beforeEvaluate mutated rules');\n }\n }\n\n async assertParamsValidation(\n modelName: string,\n invalidParams: unknown[],\n context: PluginContext,\n ): Promise<void> {\n this.plugin.onInit(context);\n\n const model = this.findModel(modelName, context);\n if (!model) {\n throw new Error(`Model \"${modelName}\" not found in plugin`);\n }\n\n for (const params of invalidParams) {\n const result = model.validateParams(params);\n if (result.valid) {\n throw new Error(\n `Expected model \"${modelName}\" to reject params: ${JSON.stringify(params)}`,\n );\n }\n }\n }\n\n async assertNoSideEffects(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void> {\n if (this.plugin.beforeEvaluate) {\n const r1 = this.plugin.beforeEvaluate(input, rules);\n const r2 = this.plugin.beforeEvaluate(input, rules);\n\n if (JSON.stringify(r1) !== JSON.stringify(r2)) {\n throw new Error('Side effects detected: different results between calls');\n }\n }\n }\n\n async runAll(\n input: EvaluationInput,\n rules: ReadonlyArray<Rule>,\n context: PluginContext,\n ): Promise<TestReport> {\n const passed: string[] = [];\n const failed: Array<{ test: string; error: string }> = [];\n\n const tests = [\n { name: 'determinism', fn: () => this.assertDeterminism(input, rules, context) },\n { name: 'immutability', fn: () => this.assertImmutability(input, rules) },\n { name: 'noSideEffects', fn: () => this.assertNoSideEffects(input, rules) },\n ];\n\n for (const t of tests) {\n try {\n await t.fn();\n passed.push(t.name);\n } catch (error) {\n failed.push({\n test: t.name,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return {\n passed,\n failed,\n summary: failed.length === 0 ? 'PASS' : 'FAIL',\n };\n }\n\n private findModel(name: string, context: PluginContext): CalculationModel | undefined {\n try {\n return context.modelRegistry.get(name);\n } catch {\n return undefined;\n }\n }\n}\n","import type { ValidationResult } from '@run-iq/core';\n\nexport interface SchemaFieldDef {\n readonly type: 'number' | 'string' | 'boolean' | 'array' | 'object';\n readonly min?: number | undefined;\n readonly max?: number | undefined;\n readonly enum?: readonly string[] | undefined;\n readonly required?: boolean | undefined;\n}\n\nexport type SchemaDefinition = Record<string, SchemaFieldDef>;\n\nexport class SchemaValidator {\n static validate(params: unknown, schema: SchemaDefinition): ValidationResult {\n const errors: string[] = [];\n\n if (params === null || typeof params !== 'object') {\n return { valid: false, errors: ['params must be a non-null object'] };\n }\n\n const obj = params as Record<string, unknown>;\n\n for (const [key, def] of Object.entries(schema)) {\n const value = obj[key];\n const isRequired = def.required !== false;\n\n if (value === undefined || value === null) {\n if (isRequired) {\n errors.push(`\"${key}\" is required`);\n }\n continue;\n }\n\n if (def.type === 'number') {\n if (typeof value !== 'number' || Number.isNaN(value)) {\n errors.push(`\"${key}\" must be a number`);\n continue;\n }\n if (def.min !== undefined && value < def.min) {\n errors.push(`\"${key}\" must be >= ${def.min}`);\n }\n if (def.max !== undefined && value > def.max) {\n errors.push(`\"${key}\" must be <= ${def.max}`);\n }\n } else if (def.type === 'string') {\n if (typeof value !== 'string') {\n errors.push(`\"${key}\" must be a string`);\n continue;\n }\n if (def.enum && !def.enum.includes(value)) {\n errors.push(`\"${key}\" must be one of: ${def.enum.join(', ')}`);\n }\n } else if (def.type === 'boolean') {\n if (typeof value !== 'boolean') {\n errors.push(`\"${key}\" must be a boolean`);\n }\n } else if (def.type === 'array') {\n if (!Array.isArray(value)) {\n errors.push(`\"${key}\" must be an array`);\n }\n } else if (def.type === 'object') {\n if (typeof value !== 'object' || Array.isArray(value)) {\n errors.push(`\"${key}\" must be an object`);\n }\n }\n }\n\n return errors.length > 0 ? { valid: false, errors } : { valid: true };\n }\n\n static number(value: unknown, opts?: { min?: number; max?: number }): boolean {\n if (typeof value !== 'number' || Number.isNaN(value)) return false;\n if (opts?.min !== undefined && value < opts.min) return false;\n if (opts?.max !== undefined && value > opts.max) return false;\n return true;\n }\n\n static string(value: unknown, opts?: { enum?: readonly string[] }): boolean {\n if (typeof value !== 'string') return false;\n if (opts?.enum && !opts.enum.includes(value)) return false;\n return true;\n }\n\n static positiveNumber(value: unknown): boolean {\n return SchemaValidator.number(value, { min: 0 });\n }\n\n static rate(value: unknown): boolean {\n return SchemaValidator.number(value, { min: 0, max: 1 });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUO,IAAe,aAAf,MAA+C;AAAA,EAKpD,OAAO,SAA8B;AACnC,eAAW,SAAS,KAAK,QAAQ;AAC/B,cAAQ,cAAc,SAAS,KAAK;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,eAAe,OAAwB,QAA8C;AACnF,WAAO;AAAA,EACT;AAAA,EAEA,cAAc,QAAyB,QAA4C;AACjF,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,QAAkB,QAA+B;AAAA,EAEzD;AAAA,EAEA,WAAiB;AAAA,EAEjB;AACF;;;AClCO,IAAe,YAAf,MAAqD;AAW5D;;;ACCO,IAAM,eAAN,MAAmB;AAAA,EACP;AAAA,EAEjB,YAAY,QAAmB;AAC7B,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,kBACJ,OACA,OACA,SACe;AACf,SAAK,OAAO,OAAO,OAAO;AAE1B,UAAM,UAA6B,CAAC;AACpC,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAI,KAAK,OAAO,gBAAgB;AAC9B,gBAAQ,KAAK,KAAK,OAAO,eAAe,OAAO,KAAK,CAAC;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,QAAQ,KAAK,UAAU,QAAQ,CAAC,CAAC;AACvC,YAAM,QAAQ,KAAK,UAAU,QAAQ,CAAC,CAAC;AACvC,YAAM,QAAQ,KAAK,UAAU,QAAQ,CAAC,CAAC;AACvC,UAAI,UAAU,SAAS,UAAU,OAAO;AACtC,cAAM,IAAI,MAAM,yDAAyD;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,mBAAmB,OAAwB,OAA2C;AAC1F,UAAM,YAAY,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAClD,UAAM,YAAY,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAElD,QAAI,KAAK,OAAO,gBAAgB;AAC9B,WAAK,OAAO,eAAe,OAAO,KAAK;AAAA,IACzC;AAEA,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,SAAS,GAAG;AACvD,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AACA,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,SAAS,GAAG;AACvD,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AAAA,EACF;AAAA,EAEA,MAAM,uBACJ,WACA,eACA,SACe;AACf,SAAK,OAAO,OAAO,OAAO;AAE1B,UAAM,QAAQ,KAAK,UAAU,WAAW,OAAO;AAC/C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,UAAU,SAAS,uBAAuB;AAAA,IAC5D;AAEA,eAAW,UAAU,eAAe;AAClC,YAAM,SAAS,MAAM,eAAe,MAAM;AAC1C,UAAI,OAAO,OAAO;AAChB,cAAM,IAAI;AAAA,UACR,mBAAmB,SAAS,uBAAuB,KAAK,UAAU,MAAM,CAAC;AAAA,QAC3E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,oBAAoB,OAAwB,OAA2C;AAC3F,QAAI,KAAK,OAAO,gBAAgB;AAC9B,YAAM,KAAK,KAAK,OAAO,eAAe,OAAO,KAAK;AAClD,YAAM,KAAK,KAAK,OAAO,eAAe,OAAO,KAAK;AAElD,UAAI,KAAK,UAAU,EAAE,MAAM,KAAK,UAAU,EAAE,GAAG;AAC7C,cAAM,IAAI,MAAM,wDAAwD;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,OACJ,OACA,OACA,SACqB;AACrB,UAAM,SAAmB,CAAC;AAC1B,UAAM,SAAiD,CAAC;AAExD,UAAM,QAAQ;AAAA,MACZ,EAAE,MAAM,eAAe,IAAI,MAAM,KAAK,kBAAkB,OAAO,OAAO,OAAO,EAAE;AAAA,MAC/E,EAAE,MAAM,gBAAgB,IAAI,MAAM,KAAK,mBAAmB,OAAO,KAAK,EAAE;AAAA,MACxE,EAAE,MAAM,iBAAiB,IAAI,MAAM,KAAK,oBAAoB,OAAO,KAAK,EAAE;AAAA,IAC5E;AAEA,eAAW,KAAK,OAAO;AACrB,UAAI;AACF,cAAM,EAAE,GAAG;AACX,eAAO,KAAK,EAAE,IAAI;AAAA,MACpB,SAAS,OAAO;AACd,eAAO,KAAK;AAAA,UACV,MAAM,EAAE;AAAA,UACR,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,SAAS,OAAO,WAAW,IAAI,SAAS;AAAA,IAC1C;AAAA,EACF;AAAA,EAEQ,UAAU,MAAc,SAAsD;AACpF,QAAI;AACF,aAAO,QAAQ,cAAc,IAAI,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AC1HO,IAAM,kBAAN,MAAM,iBAAgB;AAAA,EAC3B,OAAO,SAAS,QAAiB,QAA4C;AAC3E,UAAM,SAAmB,CAAC;AAE1B,QAAI,WAAW,QAAQ,OAAO,WAAW,UAAU;AACjD,aAAO,EAAE,OAAO,OAAO,QAAQ,CAAC,kCAAkC,EAAE;AAAA,IACtE;AAEA,UAAM,MAAM;AAEZ,eAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC/C,YAAM,QAAQ,IAAI,GAAG;AACrB,YAAM,aAAa,IAAI,aAAa;AAEpC,UAAI,UAAU,UAAa,UAAU,MAAM;AACzC,YAAI,YAAY;AACd,iBAAO,KAAK,IAAI,GAAG,eAAe;AAAA,QACpC;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU;AACzB,YAAI,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,GAAG;AACpD,iBAAO,KAAK,IAAI,GAAG,oBAAoB;AACvC;AAAA,QACF;AACA,YAAI,IAAI,QAAQ,UAAa,QAAQ,IAAI,KAAK;AAC5C,iBAAO,KAAK,IAAI,GAAG,gBAAgB,IAAI,GAAG,EAAE;AAAA,QAC9C;AACA,YAAI,IAAI,QAAQ,UAAa,QAAQ,IAAI,KAAK;AAC5C,iBAAO,KAAK,IAAI,GAAG,gBAAgB,IAAI,GAAG,EAAE;AAAA,QAC9C;AAAA,MACF,WAAW,IAAI,SAAS,UAAU;AAChC,YAAI,OAAO,UAAU,UAAU;AAC7B,iBAAO,KAAK,IAAI,GAAG,oBAAoB;AACvC;AAAA,QACF;AACA,YAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,KAAK,GAAG;AACzC,iBAAO,KAAK,IAAI,GAAG,qBAAqB,IAAI,KAAK,KAAK,IAAI,CAAC,EAAE;AAAA,QAC/D;AAAA,MACF,WAAW,IAAI,SAAS,WAAW;AACjC,YAAI,OAAO,UAAU,WAAW;AAC9B,iBAAO,KAAK,IAAI,GAAG,qBAAqB;AAAA,QAC1C;AAAA,MACF,WAAW,IAAI,SAAS,SAAS;AAC/B,YAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,iBAAO,KAAK,IAAI,GAAG,oBAAoB;AAAA,QACzC;AAAA,MACF,WAAW,IAAI,SAAS,UAAU;AAChC,YAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACrD,iBAAO,KAAK,IAAI,GAAG,qBAAqB;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAEA,WAAO,OAAO,SAAS,IAAI,EAAE,OAAO,OAAO,OAAO,IAAI,EAAE,OAAO,KAAK;AAAA,EACtE;AAAA,EAEA,OAAO,OAAO,OAAgB,MAAgD;AAC5E,QAAI,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,EAAG,QAAO;AAC7D,QAAI,MAAM,QAAQ,UAAa,QAAQ,KAAK,IAAK,QAAO;AACxD,QAAI,MAAM,QAAQ,UAAa,QAAQ,KAAK,IAAK,QAAO;AACxD,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,OAAO,OAAgB,MAA8C;AAC1E,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,MAAM,QAAQ,CAAC,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACrD,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,eAAe,OAAyB;AAC7C,WAAO,iBAAgB,OAAO,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,EACjD;AAAA,EAEA,OAAO,KAAK,OAAyB;AACnC,WAAO,iBAAgB,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK,EAAE,CAAC;AAAA,EACzD;AACF;","names":[]}
@@ -0,0 +1,62 @@
1
+ import { PPEPlugin, CalculationModel, PluginContext, EvaluationInput, Rule, EvaluationResult, PPEError, ValidationResult } from '@run-iq/core';
2
+ export { BreakdownItem, CalculationModel, DSLEvaluator, EvaluationInput, EvaluationResult, EvaluationTrace, Expression, ISnapshotAdapter, PPEPlugin, PluginContext, Rule, SkipReason, SkippedRule, Snapshot, TraceStep, ValidationResult } from '@run-iq/core';
3
+
4
+ declare abstract class BasePlugin implements PPEPlugin {
5
+ abstract readonly name: string;
6
+ abstract readonly version: string;
7
+ abstract readonly models: CalculationModel[];
8
+ onInit(context: PluginContext): void;
9
+ beforeEvaluate(input: EvaluationInput, _rules: ReadonlyArray<Rule>): EvaluationInput;
10
+ afterEvaluate(_input: EvaluationInput, result: EvaluationResult): EvaluationResult;
11
+ onError(_error: PPEError, _input: EvaluationInput): void;
12
+ teardown(): void;
13
+ }
14
+
15
+ declare abstract class BaseModel implements CalculationModel {
16
+ abstract readonly name: string;
17
+ abstract readonly version: string;
18
+ abstract validateParams(params: unknown): ValidationResult;
19
+ abstract calculate(input: Record<string, unknown>, matchedRule: Readonly<Rule>, params: unknown): number;
20
+ }
21
+
22
+ interface TestReport {
23
+ readonly passed: string[];
24
+ readonly failed: Array<{
25
+ test: string;
26
+ error: string;
27
+ }>;
28
+ readonly summary: 'PASS' | 'FAIL';
29
+ }
30
+ declare class PluginTester {
31
+ private readonly plugin;
32
+ constructor(plugin: PPEPlugin);
33
+ assertDeterminism(input: EvaluationInput, rules: ReadonlyArray<Rule>, context: PluginContext): Promise<void>;
34
+ assertImmutability(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void>;
35
+ assertParamsValidation(modelName: string, invalidParams: unknown[], context: PluginContext): Promise<void>;
36
+ assertNoSideEffects(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void>;
37
+ runAll(input: EvaluationInput, rules: ReadonlyArray<Rule>, context: PluginContext): Promise<TestReport>;
38
+ private findModel;
39
+ }
40
+
41
+ interface SchemaFieldDef {
42
+ readonly type: 'number' | 'string' | 'boolean' | 'array' | 'object';
43
+ readonly min?: number | undefined;
44
+ readonly max?: number | undefined;
45
+ readonly enum?: readonly string[] | undefined;
46
+ readonly required?: boolean | undefined;
47
+ }
48
+ type SchemaDefinition = Record<string, SchemaFieldDef>;
49
+ declare class SchemaValidator {
50
+ static validate(params: unknown, schema: SchemaDefinition): ValidationResult;
51
+ static number(value: unknown, opts?: {
52
+ min?: number;
53
+ max?: number;
54
+ }): boolean;
55
+ static string(value: unknown, opts?: {
56
+ enum?: readonly string[];
57
+ }): boolean;
58
+ static positiveNumber(value: unknown): boolean;
59
+ static rate(value: unknown): boolean;
60
+ }
61
+
62
+ export { BaseModel, BasePlugin, PluginTester, type SchemaDefinition, type SchemaFieldDef, SchemaValidator, type TestReport };
@@ -0,0 +1,62 @@
1
+ import { PPEPlugin, CalculationModel, PluginContext, EvaluationInput, Rule, EvaluationResult, PPEError, ValidationResult } from '@run-iq/core';
2
+ export { BreakdownItem, CalculationModel, DSLEvaluator, EvaluationInput, EvaluationResult, EvaluationTrace, Expression, ISnapshotAdapter, PPEPlugin, PluginContext, Rule, SkipReason, SkippedRule, Snapshot, TraceStep, ValidationResult } from '@run-iq/core';
3
+
4
+ declare abstract class BasePlugin implements PPEPlugin {
5
+ abstract readonly name: string;
6
+ abstract readonly version: string;
7
+ abstract readonly models: CalculationModel[];
8
+ onInit(context: PluginContext): void;
9
+ beforeEvaluate(input: EvaluationInput, _rules: ReadonlyArray<Rule>): EvaluationInput;
10
+ afterEvaluate(_input: EvaluationInput, result: EvaluationResult): EvaluationResult;
11
+ onError(_error: PPEError, _input: EvaluationInput): void;
12
+ teardown(): void;
13
+ }
14
+
15
+ declare abstract class BaseModel implements CalculationModel {
16
+ abstract readonly name: string;
17
+ abstract readonly version: string;
18
+ abstract validateParams(params: unknown): ValidationResult;
19
+ abstract calculate(input: Record<string, unknown>, matchedRule: Readonly<Rule>, params: unknown): number;
20
+ }
21
+
22
+ interface TestReport {
23
+ readonly passed: string[];
24
+ readonly failed: Array<{
25
+ test: string;
26
+ error: string;
27
+ }>;
28
+ readonly summary: 'PASS' | 'FAIL';
29
+ }
30
+ declare class PluginTester {
31
+ private readonly plugin;
32
+ constructor(plugin: PPEPlugin);
33
+ assertDeterminism(input: EvaluationInput, rules: ReadonlyArray<Rule>, context: PluginContext): Promise<void>;
34
+ assertImmutability(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void>;
35
+ assertParamsValidation(modelName: string, invalidParams: unknown[], context: PluginContext): Promise<void>;
36
+ assertNoSideEffects(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void>;
37
+ runAll(input: EvaluationInput, rules: ReadonlyArray<Rule>, context: PluginContext): Promise<TestReport>;
38
+ private findModel;
39
+ }
40
+
41
+ interface SchemaFieldDef {
42
+ readonly type: 'number' | 'string' | 'boolean' | 'array' | 'object';
43
+ readonly min?: number | undefined;
44
+ readonly max?: number | undefined;
45
+ readonly enum?: readonly string[] | undefined;
46
+ readonly required?: boolean | undefined;
47
+ }
48
+ type SchemaDefinition = Record<string, SchemaFieldDef>;
49
+ declare class SchemaValidator {
50
+ static validate(params: unknown, schema: SchemaDefinition): ValidationResult;
51
+ static number(value: unknown, opts?: {
52
+ min?: number;
53
+ max?: number;
54
+ }): boolean;
55
+ static string(value: unknown, opts?: {
56
+ enum?: readonly string[];
57
+ }): boolean;
58
+ static positiveNumber(value: unknown): boolean;
59
+ static rate(value: unknown): boolean;
60
+ }
61
+
62
+ export { BaseModel, BasePlugin, PluginTester, type SchemaDefinition, type SchemaFieldDef, SchemaValidator, type TestReport };
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ // src/base/BasePlugin.ts
2
+ var BasePlugin = class {
3
+ onInit(context) {
4
+ for (const model of this.models) {
5
+ context.modelRegistry.register(model);
6
+ }
7
+ }
8
+ beforeEvaluate(input, _rules) {
9
+ return input;
10
+ }
11
+ afterEvaluate(_input, result) {
12
+ return result;
13
+ }
14
+ onError(_error, _input) {
15
+ }
16
+ teardown() {
17
+ }
18
+ };
19
+
20
+ // src/base/BaseModel.ts
21
+ var BaseModel = class {
22
+ };
23
+
24
+ // src/testing/PluginTester.ts
25
+ var PluginTester = class {
26
+ plugin;
27
+ constructor(plugin) {
28
+ this.plugin = plugin;
29
+ }
30
+ async assertDeterminism(input, rules, context) {
31
+ this.plugin.onInit(context);
32
+ const results = [];
33
+ for (let i = 0; i < 3; i++) {
34
+ if (this.plugin.beforeEvaluate) {
35
+ results.push(this.plugin.beforeEvaluate(input, rules));
36
+ }
37
+ }
38
+ if (results.length === 3) {
39
+ const json0 = JSON.stringify(results[0]);
40
+ const json1 = JSON.stringify(results[1]);
41
+ const json2 = JSON.stringify(results[2]);
42
+ if (json0 !== json1 || json1 !== json2) {
43
+ throw new Error("Determinism violation: different results for same input");
44
+ }
45
+ }
46
+ }
47
+ async assertImmutability(input, rules) {
48
+ const inputCopy = JSON.parse(JSON.stringify(input));
49
+ const rulesCopy = JSON.parse(JSON.stringify(rules));
50
+ if (this.plugin.beforeEvaluate) {
51
+ this.plugin.beforeEvaluate(input, rules);
52
+ }
53
+ if (JSON.stringify(input) !== JSON.stringify(inputCopy)) {
54
+ throw new Error("Immutability violation: beforeEvaluate mutated input");
55
+ }
56
+ if (JSON.stringify(rules) !== JSON.stringify(rulesCopy)) {
57
+ throw new Error("Immutability violation: beforeEvaluate mutated rules");
58
+ }
59
+ }
60
+ async assertParamsValidation(modelName, invalidParams, context) {
61
+ this.plugin.onInit(context);
62
+ const model = this.findModel(modelName, context);
63
+ if (!model) {
64
+ throw new Error(`Model "${modelName}" not found in plugin`);
65
+ }
66
+ for (const params of invalidParams) {
67
+ const result = model.validateParams(params);
68
+ if (result.valid) {
69
+ throw new Error(
70
+ `Expected model "${modelName}" to reject params: ${JSON.stringify(params)}`
71
+ );
72
+ }
73
+ }
74
+ }
75
+ async assertNoSideEffects(input, rules) {
76
+ if (this.plugin.beforeEvaluate) {
77
+ const r1 = this.plugin.beforeEvaluate(input, rules);
78
+ const r2 = this.plugin.beforeEvaluate(input, rules);
79
+ if (JSON.stringify(r1) !== JSON.stringify(r2)) {
80
+ throw new Error("Side effects detected: different results between calls");
81
+ }
82
+ }
83
+ }
84
+ async runAll(input, rules, context) {
85
+ const passed = [];
86
+ const failed = [];
87
+ const tests = [
88
+ { name: "determinism", fn: () => this.assertDeterminism(input, rules, context) },
89
+ { name: "immutability", fn: () => this.assertImmutability(input, rules) },
90
+ { name: "noSideEffects", fn: () => this.assertNoSideEffects(input, rules) }
91
+ ];
92
+ for (const t of tests) {
93
+ try {
94
+ await t.fn();
95
+ passed.push(t.name);
96
+ } catch (error) {
97
+ failed.push({
98
+ test: t.name,
99
+ error: error instanceof Error ? error.message : String(error)
100
+ });
101
+ }
102
+ }
103
+ return {
104
+ passed,
105
+ failed,
106
+ summary: failed.length === 0 ? "PASS" : "FAIL"
107
+ };
108
+ }
109
+ findModel(name, context) {
110
+ try {
111
+ return context.modelRegistry.get(name);
112
+ } catch {
113
+ return void 0;
114
+ }
115
+ }
116
+ };
117
+
118
+ // src/validation/SchemaValidator.ts
119
+ var SchemaValidator = class _SchemaValidator {
120
+ static validate(params, schema) {
121
+ const errors = [];
122
+ if (params === null || typeof params !== "object") {
123
+ return { valid: false, errors: ["params must be a non-null object"] };
124
+ }
125
+ const obj = params;
126
+ for (const [key, def] of Object.entries(schema)) {
127
+ const value = obj[key];
128
+ const isRequired = def.required !== false;
129
+ if (value === void 0 || value === null) {
130
+ if (isRequired) {
131
+ errors.push(`"${key}" is required`);
132
+ }
133
+ continue;
134
+ }
135
+ if (def.type === "number") {
136
+ if (typeof value !== "number" || Number.isNaN(value)) {
137
+ errors.push(`"${key}" must be a number`);
138
+ continue;
139
+ }
140
+ if (def.min !== void 0 && value < def.min) {
141
+ errors.push(`"${key}" must be >= ${def.min}`);
142
+ }
143
+ if (def.max !== void 0 && value > def.max) {
144
+ errors.push(`"${key}" must be <= ${def.max}`);
145
+ }
146
+ } else if (def.type === "string") {
147
+ if (typeof value !== "string") {
148
+ errors.push(`"${key}" must be a string`);
149
+ continue;
150
+ }
151
+ if (def.enum && !def.enum.includes(value)) {
152
+ errors.push(`"${key}" must be one of: ${def.enum.join(", ")}`);
153
+ }
154
+ } else if (def.type === "boolean") {
155
+ if (typeof value !== "boolean") {
156
+ errors.push(`"${key}" must be a boolean`);
157
+ }
158
+ } else if (def.type === "array") {
159
+ if (!Array.isArray(value)) {
160
+ errors.push(`"${key}" must be an array`);
161
+ }
162
+ } else if (def.type === "object") {
163
+ if (typeof value !== "object" || Array.isArray(value)) {
164
+ errors.push(`"${key}" must be an object`);
165
+ }
166
+ }
167
+ }
168
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
169
+ }
170
+ static number(value, opts) {
171
+ if (typeof value !== "number" || Number.isNaN(value)) return false;
172
+ if (opts?.min !== void 0 && value < opts.min) return false;
173
+ if (opts?.max !== void 0 && value > opts.max) return false;
174
+ return true;
175
+ }
176
+ static string(value, opts) {
177
+ if (typeof value !== "string") return false;
178
+ if (opts?.enum && !opts.enum.includes(value)) return false;
179
+ return true;
180
+ }
181
+ static positiveNumber(value) {
182
+ return _SchemaValidator.number(value, { min: 0 });
183
+ }
184
+ static rate(value) {
185
+ return _SchemaValidator.number(value, { min: 0, max: 1 });
186
+ }
187
+ };
188
+ export {
189
+ BaseModel,
190
+ BasePlugin,
191
+ PluginTester,
192
+ SchemaValidator
193
+ };
194
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/base/BasePlugin.ts","../src/base/BaseModel.ts","../src/testing/PluginTester.ts","../src/validation/SchemaValidator.ts"],"sourcesContent":["import type {\n PPEPlugin,\n PluginContext,\n EvaluationInput,\n EvaluationResult,\n Rule,\n CalculationModel,\n PPEError,\n} from '@run-iq/core';\n\nexport abstract class BasePlugin implements PPEPlugin {\n abstract readonly name: string;\n abstract readonly version: string;\n abstract readonly models: CalculationModel[];\n\n onInit(context: PluginContext): void {\n for (const model of this.models) {\n context.modelRegistry.register(model);\n }\n }\n\n beforeEvaluate(input: EvaluationInput, _rules: ReadonlyArray<Rule>): EvaluationInput {\n return input;\n }\n\n afterEvaluate(_input: EvaluationInput, result: EvaluationResult): EvaluationResult {\n return result;\n }\n\n onError(_error: PPEError, _input: EvaluationInput): void {\n // no-op by default\n }\n\n teardown(): void {\n // no-op by default\n }\n}\n","import type { CalculationModel, ValidationResult, Rule } from '@run-iq/core';\n\nexport abstract class BaseModel implements CalculationModel {\n abstract readonly name: string;\n abstract readonly version: string;\n\n abstract validateParams(params: unknown): ValidationResult;\n\n abstract calculate(\n input: Record<string, unknown>,\n matchedRule: Readonly<Rule>,\n params: unknown,\n ): number;\n}\n","import type {\n PPEPlugin,\n PluginContext,\n EvaluationInput,\n Rule,\n CalculationModel,\n} from '@run-iq/core';\n\nexport interface TestReport {\n readonly passed: string[];\n readonly failed: Array<{ test: string; error: string }>;\n readonly summary: 'PASS' | 'FAIL';\n}\n\nexport class PluginTester {\n private readonly plugin: PPEPlugin;\n\n constructor(plugin: PPEPlugin) {\n this.plugin = plugin;\n }\n\n async assertDeterminism(\n input: EvaluationInput,\n rules: ReadonlyArray<Rule>,\n context: PluginContext,\n ): Promise<void> {\n this.plugin.onInit(context);\n\n const results: EvaluationInput[] = [];\n for (let i = 0; i < 3; i++) {\n if (this.plugin.beforeEvaluate) {\n results.push(this.plugin.beforeEvaluate(input, rules));\n }\n }\n\n if (results.length === 3) {\n const json0 = JSON.stringify(results[0]);\n const json1 = JSON.stringify(results[1]);\n const json2 = JSON.stringify(results[2]);\n if (json0 !== json1 || json1 !== json2) {\n throw new Error('Determinism violation: different results for same input');\n }\n }\n }\n\n async assertImmutability(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void> {\n const inputCopy = JSON.parse(JSON.stringify(input)) as EvaluationInput;\n const rulesCopy = JSON.parse(JSON.stringify(rules)) as Rule[];\n\n if (this.plugin.beforeEvaluate) {\n this.plugin.beforeEvaluate(input, rules);\n }\n\n if (JSON.stringify(input) !== JSON.stringify(inputCopy)) {\n throw new Error('Immutability violation: beforeEvaluate mutated input');\n }\n if (JSON.stringify(rules) !== JSON.stringify(rulesCopy)) {\n throw new Error('Immutability violation: beforeEvaluate mutated rules');\n }\n }\n\n async assertParamsValidation(\n modelName: string,\n invalidParams: unknown[],\n context: PluginContext,\n ): Promise<void> {\n this.plugin.onInit(context);\n\n const model = this.findModel(modelName, context);\n if (!model) {\n throw new Error(`Model \"${modelName}\" not found in plugin`);\n }\n\n for (const params of invalidParams) {\n const result = model.validateParams(params);\n if (result.valid) {\n throw new Error(\n `Expected model \"${modelName}\" to reject params: ${JSON.stringify(params)}`,\n );\n }\n }\n }\n\n async assertNoSideEffects(input: EvaluationInput, rules: ReadonlyArray<Rule>): Promise<void> {\n if (this.plugin.beforeEvaluate) {\n const r1 = this.plugin.beforeEvaluate(input, rules);\n const r2 = this.plugin.beforeEvaluate(input, rules);\n\n if (JSON.stringify(r1) !== JSON.stringify(r2)) {\n throw new Error('Side effects detected: different results between calls');\n }\n }\n }\n\n async runAll(\n input: EvaluationInput,\n rules: ReadonlyArray<Rule>,\n context: PluginContext,\n ): Promise<TestReport> {\n const passed: string[] = [];\n const failed: Array<{ test: string; error: string }> = [];\n\n const tests = [\n { name: 'determinism', fn: () => this.assertDeterminism(input, rules, context) },\n { name: 'immutability', fn: () => this.assertImmutability(input, rules) },\n { name: 'noSideEffects', fn: () => this.assertNoSideEffects(input, rules) },\n ];\n\n for (const t of tests) {\n try {\n await t.fn();\n passed.push(t.name);\n } catch (error) {\n failed.push({\n test: t.name,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return {\n passed,\n failed,\n summary: failed.length === 0 ? 'PASS' : 'FAIL',\n };\n }\n\n private findModel(name: string, context: PluginContext): CalculationModel | undefined {\n try {\n return context.modelRegistry.get(name);\n } catch {\n return undefined;\n }\n }\n}\n","import type { ValidationResult } from '@run-iq/core';\n\nexport interface SchemaFieldDef {\n readonly type: 'number' | 'string' | 'boolean' | 'array' | 'object';\n readonly min?: number | undefined;\n readonly max?: number | undefined;\n readonly enum?: readonly string[] | undefined;\n readonly required?: boolean | undefined;\n}\n\nexport type SchemaDefinition = Record<string, SchemaFieldDef>;\n\nexport class SchemaValidator {\n static validate(params: unknown, schema: SchemaDefinition): ValidationResult {\n const errors: string[] = [];\n\n if (params === null || typeof params !== 'object') {\n return { valid: false, errors: ['params must be a non-null object'] };\n }\n\n const obj = params as Record<string, unknown>;\n\n for (const [key, def] of Object.entries(schema)) {\n const value = obj[key];\n const isRequired = def.required !== false;\n\n if (value === undefined || value === null) {\n if (isRequired) {\n errors.push(`\"${key}\" is required`);\n }\n continue;\n }\n\n if (def.type === 'number') {\n if (typeof value !== 'number' || Number.isNaN(value)) {\n errors.push(`\"${key}\" must be a number`);\n continue;\n }\n if (def.min !== undefined && value < def.min) {\n errors.push(`\"${key}\" must be >= ${def.min}`);\n }\n if (def.max !== undefined && value > def.max) {\n errors.push(`\"${key}\" must be <= ${def.max}`);\n }\n } else if (def.type === 'string') {\n if (typeof value !== 'string') {\n errors.push(`\"${key}\" must be a string`);\n continue;\n }\n if (def.enum && !def.enum.includes(value)) {\n errors.push(`\"${key}\" must be one of: ${def.enum.join(', ')}`);\n }\n } else if (def.type === 'boolean') {\n if (typeof value !== 'boolean') {\n errors.push(`\"${key}\" must be a boolean`);\n }\n } else if (def.type === 'array') {\n if (!Array.isArray(value)) {\n errors.push(`\"${key}\" must be an array`);\n }\n } else if (def.type === 'object') {\n if (typeof value !== 'object' || Array.isArray(value)) {\n errors.push(`\"${key}\" must be an object`);\n }\n }\n }\n\n return errors.length > 0 ? { valid: false, errors } : { valid: true };\n }\n\n static number(value: unknown, opts?: { min?: number; max?: number }): boolean {\n if (typeof value !== 'number' || Number.isNaN(value)) return false;\n if (opts?.min !== undefined && value < opts.min) return false;\n if (opts?.max !== undefined && value > opts.max) return false;\n return true;\n }\n\n static string(value: unknown, opts?: { enum?: readonly string[] }): boolean {\n if (typeof value !== 'string') return false;\n if (opts?.enum && !opts.enum.includes(value)) return false;\n return true;\n }\n\n static positiveNumber(value: unknown): boolean {\n return SchemaValidator.number(value, { min: 0 });\n }\n\n static rate(value: unknown): boolean {\n return SchemaValidator.number(value, { min: 0, max: 1 });\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@run-iq/plugin-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Plugin SDK for PPE — BasePlugin, BaseModel, PluginTester",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "eslint src/ tests/ && prettier --check src/ tests/",
27
+ "lint:fix": "eslint src/ tests/ --fix && prettier --write src/ tests/"
28
+ },
29
+ "peerDependencies": {
30
+ "@run-iq/core": "^0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.11.0",
34
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
35
+ "@typescript-eslint/parser": "^7.0.0",
36
+ "eslint": "^8.57.0",
37
+ "prettier": "^3.2.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.4.0",
40
+ "vitest": "^1.3.0"
41
+ },
42
+ "author": "Abdou-Raouf ATARMLA",
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ }
47
+ }