← run

ts-10-rule-engine

0.000
0/1 tests· architecture
Challenge · difficulty 5/5
# Typed rule / query engine

Implement a file **`solution.ts`** that interprets a small, tree-shaped rule language
over plain records and uses it to filter collections of records.

## Types to export

```ts
export type Rule =
  | { op: "and"; rules: Rule[] }
  | { op: "or"; rules: Rule[] }
  | { op: "not"; rule: Rule }
  | { op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte"; field: string; value: number | string }
  | { op: "in"; field: string; values: Array<number | string> };

export class RuleError extends Error {}

export function evaluate(rule: Rule, record: Record<string, unknown>): boolean;

export function filter(
  rule: Rule,
  records: Array<Record<string, unknown>>,
): Array<Record<string, unknown>>;
```

## Semantics

`evaluate(rule, record)` returns a boolean by recursively interpreting `rule` against a
single `record`.

### Logical combinators

- `and`: true iff **every** sub-rule in `rules` is true. An empty `rules` array is
  **true** (the identity of conjunction).
- `or`: true iff **any** sub-rule in `rules` is true. An empty `rules` array is
  **false** (the identity of disjunction).
- `not`: the negation of its single child `rule`.

### Comparisons

Comparison ops look up `record[field]`. Let `fieldValue = record[field]`.

- `eq`: true iff `fieldValue === value`.
- `ne`: true iff `fieldValue !== value`.
- `lt` / `lte` / `gt` / `gte`: ordering comparisons. They are evaluated **only** when
  `fieldValue` and `value` have the same comparable type:
  - if **both** are numbers, compare numerically;
  - if **both** are strings, compare lexicographically (with `<`, `<=`, `>`, `>=`);
  - otherwise (the field is missing/`undefined`, or one side is a number and the other
    a string, or the field holds some other type) the comparison is **`false`**. It does
    **not** throw.

So `{ op: "gt", field: "age", value: 18 }` is `false` for a record with no `age`, and
also `false` if `age` is the string `"20"` (string vs number mismatch).

### Membership

- `in`: true iff `record[field]` is **strictly equal** (`===`) to at least one element
  of `values`.

### Validation — when to throw `RuleError`

A rule node is invalid if it is not a well-formed member of the `Rule` union. Invalid
nodes must cause `evaluate` (and therefore `filter`) to throw a `RuleError` (not a plain
`Error`). In particular, throw `RuleError` when:

- `op` is missing or is not one of the known operators;
- `and` / `or` does not carry an array `rules`;
- `not` does not carry a child `rule` object;
- a comparison op (`eq`/`ne`/`lt`/`lte`/`gt`/`gte`) does not carry a string `field`, or
  its `value` is neither a number nor a string;
- `in` does not carry a string `field` or an array `values`.

You may validate up front or lazily as you evaluate; either is fine, as long as invalid
rules raise `RuleError`.

## `filter`

`filter(rule, records)` returns a new array containing exactly the records for which
`evaluate(rule, record)` is `true`, **in their original order**. If `rule` is invalid it
propagates the `RuleError`.

## Worked example

```ts
const rule: Rule = {
  op: "and",
  rules: [
    { op: "gte", field: "age", value: 18 },
    {
      op: "or",
      rules: [
        { op: "eq", field: "country", value: "US" },
        { op: "in", field: "country", values: ["CA", "MX"] },
      ],
    },
    { op: "not", rule: { op: "eq", field: "banned", value: 1 } },
  ],
};

const people = [
  { name: "a", age: 25, country: "US", banned: 0 }, // kept
  { name: "b", age: 17, country: "US", banned: 0 }, // dropped: age < 18
  { name: "c", age: 40, country: "CA", banned: 1 }, // dropped: banned
  { name: "d", age: 30, country: "FR", banned: 0 }, // dropped: country
  { name: "e", age: 22, country: "MX", banned: 0 }, // kept
];

filter(rule, people); // -> [ {name:"a",...}, {name:"e",...} ]
```

Keep it fully typed: `solution.ts` must pass `tsc --noEmit` in strict mode. Do not use
`any` in the implementation; prefer `unknown` and narrow.
tests/solution.test.ts
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { evaluate, filter, RuleError, type Rule } from "./solution.ts";

test("eq / ne on numbers and strings", () => {
  assert.equal(evaluate({ op: "eq", field: "a", value: 1 }, { a: 1 }), true);
  assert.equal(evaluate({ op: "eq", field: "a", value: 1 }, { a: 2 }), false);
  assert.equal(evaluate({ op: "ne", field: "a", value: 1 }, { a: 2 }), true);
  assert.equal(evaluate({ op: "eq", field: "s", value: "x" }, { s: "x" }), true);
  assert.equal(evaluate({ op: "ne", field: "s", value: "x" }, { s: "y" }), true);
});

test("eq does not coerce types (number vs string)", () => {
  assert.equal(evaluate({ op: "eq", field: "a", value: 1 }, { a: "1" }), false);
  assert.equal(evaluate({ op: "ne", field: "a", value: 1 }, { a: "1" }), true);
});

test("numeric lt / lte / gt / gte", () => {
  assert.equal(evaluate({ op: "lt", field: "n", value: 10 }, { n: 5 }), true);
  assert.equal(evaluate({ op: "lt", field: "n", value: 10 }, { n: 10 }), false);
  assert.equal(evaluate({ op: "lte", field: "n", value: 10 }, { n: 10 }), true);
  assert.equal(evaluate({ op: "gt", field: "n", value: 10 }, { n: 11 }), true);
  assert.equal(evaluate({ op: "gt", field: "n", value: 10 }, { n: 10 }), false);
  assert.equal(evaluate({ op: "gte", field: "n", value: 10 }, { n: 10 }), true);
});

test("lexicographic string ordering", () => {
  assert.equal(evaluate({ op: "lt", field: "s", value: "banana" }, { s: "apple" }), true);
  assert.equal(evaluate({ op: "gt", field: "s", value: "apple" }, { s: "banana" }), true);
  assert.equal(evaluate({ op: "gte", field: "s", value: "apple" }, { s: "apple" }), true);
  assert.equal(evaluate({ op: "lte", field: "s", value: "apple" }, { s: "apple" }), true);
});

test("ordering with missing field is false", () => {
  assert.equal(evaluate({ op: "gt", field: "age", value: 18 }, {}), false);
  assert.equal(evaluate({ op: "lt", field: "age", value: 18 }, {}), false);
  assert.equal(evaluate({ op: "lte", field: "age", value: 18 }, {}), false);
  assert.equal(evaluate({ op: "gte", field: "age", value: 18 }, {}), false);
});

test("ordering with type mismatch is false (no throw)", () => {
  // number value, string field
  assert.equal(evaluate({ op: "gt", field: "age", value: 18 }, { age: "20" }), false);
  // string value, number field
  assert.equal(evaluate({ op: "lt", field: "age", value: "20" }, { age: 18 }), false);
  // field holds a non-comparable type
  assert.equal(evaluate({ op: "gt", field: "age", value: 1 }, { age: true }), false);
});

test("eq with missing field is false, ne is true", () => {
  assert.equal(evaluate({ op: "eq", field: "x", value: 1 }, {}), false);
  assert.equal(evaluate({ op: "ne", field: "x", value: 1 }, {}), true);
});

test("in membership", () => {
  assert.equal(evaluate({ op: "in", field: "c", values: ["US", "CA"] }, { c: "CA" }), true);
  assert.equal(evaluate({ op: "in", field: "c", values: ["US", "CA"] }, { c: "FR" }), false);
  assert.equal(evaluate({ op: "in", field: "n", values: [1, 2, 3] }, { n: 2 }), true);
  // strict equality: "1" is not 1
  assert.equal(evaluate({ op: "in", field: "n", values: [1, 2, 3] }, { n: "1" }), false);
  // empty values -> never matches
  assert.equal(evaluate({ op: "in", field: "n", values: [] }, { n: 1 }), false);
});

test("not negates its child", () => {
  assert.equal(evaluate({ op: "not", rule: { op: "eq", field: "a", value: 1 } }, { a: 1 }), false);
  assert.equal(evaluate({ op: "not", rule: { op: "eq", field: "a", value: 1 } }, { a: 2 }), true);
});

test("empty and is true, empty or is false", () => {
  assert.equal(evaluate({ op: "and", rules: [] }, {}), true);
  assert.equal(evaluate({ op: "or", rules: [] }, {}), false);
});

test("and / or short-circuit semantics over multiple rules", () => {
  const r: Rule = {
    op: "and",
    rules: [
      { op: "gte", field: "age", value: 18 },
      { op: "eq", field: "country", value: "US" },
    ],
  };
  assert.equal(evaluate(r, { age: 20, country: "US" }), true);
  assert.equal(evaluate(r, { age: 17, country: "US" }), false);
  assert.equal(evaluate(r, { age: 20, country: "FR" }), false);

  const o: Rule = {
    op: "or",
    rules: [
      { op: "eq", field: "country", value: "US" },
      { op: "eq", field: "country", value: "CA" },
    ],
  };
  assert.equal(evaluate(o, { country: "CA" }), true);
  assert.equal(evaluate(o, { country: "FR" }), false);
});

test("deeply nested and / or / not", () => {
  const rule: Rule = {
    op: "and",
    rules: [
      { op: "gte", field: "age", value: 18 },
      {
        op: "or",
        rules: [
          { op: "eq", field: "country", value: "US" },
          { op: "in", field: "country", values: ["CA", "MX"] },
        ],
      },
      { op: "not", rule: { op: "eq", field: "banned", value: 1 } },
    ],
  };
  assert.equal(evaluate(rule, { age: 25, country: "US", banned: 0 }), true);
  assert.equal(evaluate(rule, { age: 17, country: "US", banned: 0 }), false);
  assert.equal(evaluate(rule, { age: 40, country: "CA", banned: 1 }), false);
  assert.equal(evaluate(rule, { age: 30, country: "FR", banned: 0 }), false);
  assert.equal(evaluate(rule, { age: 22, country: "MX", banned: 0 }), true);
});

test("filter keeps matching records in original order", () => {
  const rule: Rule = {
    op: "and",
    rules: [
      { op: "gte", field: "age", value: 18 },
      {
        op: "or",
        rules: [
          { op: "eq", field: "country", value: "US" },
          { op: "in", field: "country", values: ["CA", "MX"] },
        ],
      },
      { op: "not", rule: { op: "eq", field: "banned", value: 1 } },
    ],
  };
  const people = [
    { name: "a", age: 25, country: "US", banned: 0 },
    { name: "b", age: 17, country: "US", banned: 0 },
    { name: "c", age: 40, country: "CA", banned: 1 },
    { name: "d", age: 30, country: "FR", banned: 0 },
    { name: "e", age: 22, country: "MX", banned: 0 },
  ];
  assert.deepEqual(
    filter(rule, people).map((r) => r["name"]),
    ["a", "e"],
  );
});

test("filter with empty-and matches everything; empty-or matches nothing", () => {
  const recs = [{ a: 1 }, { a: 2 }, { a: 3 }];
  assert.deepEqual(filter({ op: "and", rules: [] }, recs), recs);
  assert.deepEqual(filter({ op: "or", rules: [] }, recs), []);
});

test("RuleError thrown for an unknown op", () => {
  assert.throws(() => evaluate({ op: "wat" } as unknown as Rule, {}), RuleError);
});

test("RuleError thrown for malformed and / not / comparison nodes", () => {
  // and without an array rules
  assert.throws(() => evaluate({ op: "and" } as unknown as Rule, {}), RuleError);
  // not without a child rule
  assert.throws(() => evaluate({ op: "not" } as unknown as Rule, {}), RuleError);
  // comparison without a string field
  assert.throws(() => evaluate({ op: "eq", value: 1 } as unknown as Rule, {}), RuleError);
  // comparison with a non number/string value
  assert.throws(
    () => evaluate({ op: "gt", field: "a", value: true } as unknown as Rule, {}),
    RuleError,
  );
  // in without an array values
  assert.throws(() => evaluate({ op: "in", field: "a" } as unknown as Rule, {}), RuleError);
  // missing op entirely
  assert.throws(() => evaluate({} as unknown as Rule, {}), RuleError);
});

test("filter propagates RuleError from an invalid rule", () => {
  assert.throws(() => filter({ op: "nope" } as unknown as Rule, [{ a: 1 }]), RuleError);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```typescript
export type Rule =
  | { op: "and"; rules: Rule[] }
  | { op: "or"; rules: Rule[] }
  | { op: "not"; rule: Rule }
  | { op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte"; field: string; value: number | string }
  | { op: "in"; field: string; values: Array<number | string> };

export class RuleError extends Error {}

function isNumber(value: unknown): value is number {
  return typeof value === "number" && !Number.isNaN(value);
}

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function evaluate(rule: Rule, record: Record<string, unknown>): boolean {
  switch (rule.op) {
    case "and": {
      if (!Array.isArray(rule.rules)) {
        throw new RuleError("Invalid 'and' rule: 'rules' must be an array.");
      }
      return rule.rules.every((r) => evaluate(r, record));
    }
    case "or": {
      if (!Array.isArray(rule.rules)) {
        throw new RuleError("Invalid 'or' rule: 'rules' must be an array.");
      }
      return rule.rules.some((r) => evaluate(r, record));
    }
    case "not": {
      if (!rule.rule || typeof rule.rule !== "object") {
        throw new RuleError("Invalid 'not' rule: 'rule' must be an object.");
      }
      return !evaluate(rule.rule, record);
    }
    case "eq":
    case "ne": {
      if (typeof rule.field !== "string") {
        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);
      }
      if (typeof rule.value !== "number" && typeof rule.value !== "string") {
        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);
      }
      const fieldValue = record[rule.field];
      if (rule.op === "eq") {
        return fieldValue === rule.value;
      } else {
        return fieldValue !== rule.value;
      }
    }
    case "lt":
    case "lte":
    case "gt":
    case "gte": {
      if (typeof rule.field !== "string") {
        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);
      }
      if (typeof rule.value !== "number" && typeof rule.value !== "string") {
        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);
      }
      const fieldValue = record[rule.field];
      if (fieldValue === undefined) {
        return false;
      }
      if (isNumber(fieldValue) && isNumber(rule.value)) {
        const result =
          rule.op === "lt"
            ? fieldValue < rule.value
            : rule.op === "lte"
              ? fieldValue <= rule.value
              : rule.op === "gt"
                ? fieldValue > rule.value
                : fieldValue >= rule.value;
        return result;
      }
      if (isString(fieldValue) && isString(rule.value)) {
        const result =
          rule.op === "lt"
            ? fieldValue < rule.value
            : rule.op === "lte"
              ? fieldValue <= rule.value
              : rule.op === "gt"
                ? fieldValue > rule.value
                : fieldValue >= rule.value;
        return result;
      }
      return false;
    }
    case "in": {
      if (typeof rule.field !== "string") {
        throw new RuleError("Invalid 'in' rule: 'field' must be a string.");
      }
      if (!Array.isArray(rule.values)) {
        throw new RuleError("Invalid 'in' rule: 'values' must be an array.");
      }
      const fieldValue = record[rule.field];
      return rule.values.some((v) => v === fieldValue);
    }
    default: {
      throw new RuleError(`Invalid rule: unknown operator '${(rule as { op: string }).op}'.`);
    }
  }
}

export function evaluate(rule: Rule, record: Record<string, unknown>): boolean {
  return evaluate(rule, record);
}

export function filter(
  rule: Rule,
  records: Array<Record<string, unknown>>,
): Array<Record<string, unknown>> {
  return records.filter((record) => evaluate(rule, record));
}
```

Test output (stdout)

TAP version 13
# node:internal/modules/run_main:107
#     triggerUncaughtException(
#     ^
# Error: Transform failed with 1 error:
# /tmp/llmlab-ts-10-rule-engine-p1rh0c3s/solution.ts:107:16: ERROR: The symbol "evaluate" has already been declared
#     at failureErrorWithLog (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:1748:15)
#     at /home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:1017:50
#     at responseCallbacks.<computed> (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:884:9)
#     at handleIncomingPacket (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:939:12)
#     at Socket.readFromStdout (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:862:7)
#     at Socket.emit (node:events:509:28)
#     at addChunk (node:internal/streams/readable:563:12)
#     at readableAddChunkPushByteMode (node:internal/streams/readable:514:3)
#     at Readable.push (node:internal/streams/readable:394:5)
#     at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) {
#   name: 'TransformError'
# }
# Node.js v24.16.0
# Subtest: solution.test.ts
not ok 1 - solution.test.ts
  ---
  duration_ms: 69.708601
  type: 'test'
  location: '/tmp/llmlab-ts-10-rule-engine-p1rh0c3s/solution.test.ts:1:1'
  failureType: 'testCodeFailure'
  exitCode: 1
  signal: ~
  error: 'test failed'
  code: 'ERR_TEST_FAILURE'
  ...
1..1
# tests 1
# suites 0
# pass 0
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 73.359105
Self-repair attempts
[
  {
    "total": 1,
    "answer": "```typescript\nexport type Rule =\n  | { op: \"and\"; rules: Rule[] }\n  | { op: \"or\"; rules: Rule[] }\n  | { op: \"not\"; rule: Rule }\n  | { op: \"eq\" | \"ne\" | \"lt\" | \"lte\" | \"gt\" | \"gte\"; field: string; value: number | string }\n  | { op: \"in\"; field: string; values: Array<number | string> };\n\nexport class RuleError extends Error {}\n\nfunction isNumber(value: unknown): value is number {\n  return typeof value === \"number\" && !Number.isNaN(value);\n}\n\nfunction isString(value: unknown): value is string {\n  return typeof value === \"string\";\n}\n\nfunction evaluate(rule: Rule, record: Record<string, unknown>): boolean {\n  switch (rule.op) {\n    case \"and\": {\n      if (!Array.isArray(rule.rules)) {\n        throw new RuleError(\"Invalid 'and' rule: 'rules' must be an array.\");\n      }\n      return rule.rules.every((r) => evaluate(r, record));\n    }\n    case \"or\": {\n      if (!Array.isArray(rule.rules)) {\n        throw new RuleError(\"Invalid 'or' rule: 'rules' must be an array.\");\n      }\n      return rule.rules.some((r) => evaluate(r, record));\n    }\n    case \"not\": {\n      if (!rule.rule || typeof rule.rule !== \"object\") {\n        throw new RuleError(\"Invalid 'not' rule: 'rule' must be an object.\");\n      }\n      return !evaluate(rule.rule, record);\n    }\n    case \"eq\":\n    case \"ne\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);\n      }\n      if (typeof rule.value !== \"number\" && typeof rule.value !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);\n      }\n      const fieldValue = record[rule.field];\n      if (rule.op === \"eq\") {\n        return fieldValue === rule.value;\n      } else {\n        return fieldValue !== rule.value;\n      }\n    }\n    case \"lt\":\n    case \"lte\":\n    case \"gt\":\n    case \"gte\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);\n      }\n      if (typeof rule.value !== \"number\" && typeof rule.value !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);\n      }\n      const fieldValue = record[rule.field];\n      if (fieldValue === undefined) {\n        return false;\n      }\n      if (isNumber(fieldValue) && isNumber(rule.value)) {\n        const result =\n          rule.op === \"lt\"\n            ? fieldValue < rule.value\n            : rule.op === \"lte\"\n              ? fieldValue <= rule.value\n              : rule.op === \"gt\"\n                ? fieldValue > rule.value\n                : fieldValue >= rule.value;\n        return result;\n      }\n      if (isString(fieldValue) && isString(rule.value)) {\n        const result =\n          rule.op === \"lt\"\n            ? fieldValue < rule.value\n            : rule.op === \"lte\"\n              ? fieldValue <= rule.value\n              : rule.op === \"gt\"\n                ? fieldValue > rule.value\n                : fieldValue >= rule.value;\n        return result;\n      }\n      return false;\n    }\n    case \"in\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(\"Invalid 'in' rule: 'field' must be a string.\");\n      }\n      if (!Array.isArray(rule.values)) {\n        throw new RuleError(\"Invalid 'in' rule: 'values' must be an array.\");\n      }\n      const fieldValue = record[rule.field];\n      return rule.values.some((v) => v === fieldValue);\n    }\n    default: {\n      throw new RuleError(`Invalid rule: unknown operator '${(rule as { op: string }).op}'.`);\n    }\n  }\n}\n\nexport function evaluate(rule: Rule, record: Record<string, unknown>): boolean {\n  return evaluate(rule, record);\n}\n\nexport function filter(\n  rule: Rule,\n  records: Array<Record<string, unknown>>,\n): Array<Record<string, unknown>> {\n  return records.filter((record) => evaluate(rule, record));\n}\n```",
    "passed": 0,
    "test_error": "TAP version 13\n# node:internal/modules/run_main:107\n#     triggerUncaughtException(\n#     ^\n# Error: Transform failed with 1 error:\n# /tmp/llmlab-ts-10-rule-engine-p1rh0c3s/solution.ts:107:16: ERROR: The symbol \"evaluate\" has already been declared\n#     at failureErrorWithLog (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:1748:15)\n#     at /home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:1017:50\n#     at responseCallbacks.<computed> (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:884:9)\n#     at handleIncomingPacket (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:939:12)\n#     at Socket.readFromStdout (/home/psc/opt/node/lib/node_modules/tsx/node_modules/esbuild/lib/main.js:862:7)\n#     at Socket.emit (node:events:509:28)\n#     at addChunk (node:internal/streams/readable:563:12)\n#     at readableAddChunkPushByteMode (node:internal/streams/readable:514:3)\n#     at Readable.push (node:internal/streams/readable:394:5)\n#     at Pipe.onStreamRead (node:internal/stream_base_commons:189:23) {\n#   name: 'TransformError'\n# }\n# Node.js v24.16.0\n# Subtest: solution.test.ts\nnot ok 1 - solution.test.ts\n  ---\n  duration_ms: 69.708601\n  type: 'test'\n  location: '/tmp/llmlab-ts-10-rule-engine-p1rh0c3s/solution.test.ts:1:1'\n  failureType: 'testCodeFailure'\n  exitCode: 1\n  signal: ~\n  error: 'test failed'\n  code: 'ERR_TEST_FAILURE'\n  ...\n1..1\n# tests 1\n# suites 0\n# pass 0\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 73.359105"
  },
  {
    "total": 17,
    "answer": "```typescript\nexport type Rule =\n  | { op: \"and\"; rules: Rule[] }\n  | { op: \"or\"; rules: Rule[] }\n  | { op: \"not\"; rule: Rule }\n  | { op: \"eq\" | \"ne\" | \"lt\" | \"lte\" | \"gt\" | \"gte\"; field: string; value: number | string }\n  | { op: \"in\"; field: string; values: Array<number | string> };\n\nexport class RuleError extends Error {}\n\nfunction isNumber(value: unknown): value is number {\n  return typeof value === \"number\" && !Number.isNaN(value);\n}\n\nfunction isString(value: unknown): value is string {\n  return typeof value === \"string\";\n}\n\nfunction evaluateRule(rule: Rule, record: Record<string, unknown>): boolean {\n  switch (rule.op) {\n    case \"and\": {\n      if (!Array.isArray(rule.rules)) {\n        throw new RuleError(\"Invalid 'and' rule: 'rules' must be an array.\");\n      }\n      return rule.rules.every((r) => evaluateRule(r, record));\n    }\n    case \"or\": {\n      if (!Array.isArray(rule.rules)) {\n        throw new RuleError(\"Invalid 'or' rule: 'rules' must be an array.\");\n      }\n      return rule.rules.some((r) => evaluateRule(r, record));\n    }\n    case \"not\": {\n      if (!rule.rule || typeof rule.rule !== \"object\") {\n        throw new RuleError(\"Invalid 'not' rule: 'rule' must be an object.\");\n      }\n      return !evaluateRule(rule.rule, record);\n    }\n    case \"eq\":\n    case \"ne\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);\n      }\n      if (typeof rule.value !== \"number\" && typeof rule.value !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);\n      }\n      const fieldValue = record[rule.field];\n      if (rule.op === \"eq\") {\n        return fieldValue === rule.value;\n      } else {\n        return fieldValue !== rule.value;\n      }\n    }\n    case \"lt\":\n    case \"lte\":\n    case \"gt\":\n    case \"gte\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'field' must be a string.`);\n      }\n      if (typeof rule.value !== \"number\" && typeof rule.value !== \"string\") {\n        throw new RuleError(`Invalid '${rule.op}' rule: 'value' must be a number or string.`);\n      }\n      const fieldValue = record[rule.field];\n      if (fieldValue === undefined) {\n        return false;\n      }\n      if (isNumber(fieldValue) && isNumber(rule.value)) {\n        const result =\n          rule.op === \"lt\"\n            ? fieldValue < rule.value\n            : rule.op === \"lte\"\n              ? fieldValue <= rule.value\n              : rule.op === \"gt\"\n                ? fieldValue > rule.value\n                : fieldValue >= rule.value;\n        return result;\n      }\n      if (isString(fieldValue) && isString(rule.value)) {\n        const result =\n          rule.op === \"lt\"\n            ? fieldValue < rule.value\n            : rule.op === \"lte\"\n              ? fieldValue <= rule.value\n              : rule.op === \"gt\"\n                ? fieldValue > rule.value\n                : fieldValue >= rule.value;\n        return result;\n      }\n      return false;\n    }\n    case \"in\": {\n      if (typeof rule.field !== \"string\") {\n        throw new RuleError(\"Invalid 'in' rule: 'field' must be a string.\");\n      }\n      if (!Array.isArray(rule.values)) {\n        throw new RuleError(\"Invalid 'in' rule: 'values' must be an array.\");\n      }\n      const fieldValue = record[rule.field];\n      return rule.values.some((v) => v === fieldValue);\n    }\n    default: {\n      throw new RuleError(`Invalid rule: unknown operator '${(rule as { op: string }).op}'.`);\n    }\n  }\n}\n\nexport function evaluate(rule: Rule, record: Record<string, unknown>): boolean {\n  return evaluateRule(rule, record);\n}\n\nexport function filter(\n  rule: Rule,\n  records: Array<Record<string, unknown>>,\n): Array<Record<string, unknown>> {\n  return records.filter((record) => evaluate(rule, record));\n}\n```",
    "passed": 17,
    "test_error": ""
  }
]