ts-10-rule-engine
0.941
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
```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 {}
function isNumberOrString(value: unknown): value is number | string {
return typeof value === "number" || typeof value === "string";
}
function validateRule(rule: unknown): asserts rule is Rule {
if (typeof rule !== "object" || rule === null) {
throw new RuleError("Rule must be an object");
}
const r = rule as Record<string, unknown>;
if (!("op" in r)) {
throw new RuleError("Rule is missing 'op'");
}
const op = r.op;
if (typeof op !== "string") {
throw new RuleError("Rule 'op' must be a string");
}
switch (op) {
case "and":
case "or": {
if (!("rules" in r) || !Array.isArray(r.rules)) {
throw new RuleError(`Rule with op '${op}' must have an array 'rules'`);
}
for (const subRule of r.rules) {
validateRule(subRule);
}
break;
}
case "not": {
if (!("rule" in r)) {
throw new RuleError("Rule with op 'not' must have a 'rule' child");
}
validateRule(r.rule);
break;
}
case "eq":
case "ne":
case "lt":
case "lte":
case "gt":
case "gte": {
if (!("field" in r) || typeof r.field !== "string") {
throw new RuleError(`Rule with op '${op}' must have a string 'field'`);
}
if (!("value" in r) || !isNumberOrString(r.value)) {
throw new RuleError(`Rule with op '${op}' must have a number or string 'value'`);
}
break;
}
case "in": {
if (!("field" in r) || typeof r.field !== "string") {
throw new RuleError("Rule with op 'in' must have a string 'field'");
}
if (!("values" in r) || !Array.isArray(r.values)) {
throw new RuleError("Rule with op 'in' must have an array 'values'");
}
for (const v of r.values) {
if (!isNumberOrString(v)) {
throw new RuleError("Rule with op 'in' must have values that are numbers or strings");
}
}
break;
}
default: {
throw new RuleError(`Unknown rule operator '${op}'`);
}
}
}
function evaluateComparison(
op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte",
fieldValue: unknown,
value: number | string
): boolean {
if (fieldValue === undefined) {
return false;
}
if (op === "eq") {
return fieldValue === value;
}
if (op === "ne") {
return fieldValue !== value;
}
// For ordering comparisons, ensure both sides are of the same comparable type
if (typeof fieldValue === "number" && typeof value === "number") {
switch (op) {
case "lt":
return fieldValue < value;
case "lte":
return fieldValue <= value;
case "gt":
return fieldValue > value;
case "gte":
return fieldValue >= value;
default:
return false;
}
}
if (typeof fieldValue === "string" && typeof value === "string") {
switch (op) {
case "lt":
return fieldValue < value;
case "lte":
return fieldValue <= value;
case "gt":
return fieldValue > value;
case "gte":
return fieldValue >= value;
default:
return false;
}
}
return false;
}
export function evaluate(rule: Rule, record: Record<string, unknown>): boolean {
validateRule(rule);
switch (rule.op) {
case "and": {
for (const subRule of rule.rules) {
if (!evaluate(subRule, record)) {
return false;
}
}
return true;
}
case "or": {
for (const subRule of rule.rules) {
if (evaluate(subRule, record)) {
return true;
}
}
return false;
}
case "not": {
return !evaluate(rule.rule, record);
}
case "eq":
case "ne":
case "lt":
case "lte":
case "gt":
case "gte": {
const fieldValue = record[rule.field];
return evaluateComparison(rule.op, fieldValue, rule.value);
}
case "in": {
const fieldValue = record[rule.field];
return rule.values.some((v) => v === fieldValue);
}
default: {
// Should not reach here due to validation
throw new RuleError(`Unknown rule operator '${rule.op}'`);
}
}
}
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
# Subtest: eq / ne on numbers and strings
ok 1 - eq / ne on numbers and strings
---
duration_ms: 0.510624
type: 'test'
...
# Subtest: eq does not coerce types (number vs string)
ok 2 - eq does not coerce types (number vs string)
---
duration_ms: 0.058851
type: 'test'
...
# Subtest: numeric lt / lte / gt / gte
ok 3 - numeric lt / lte / gt / gte
---
duration_ms: 0.05475
type: 'test'
...
# Subtest: lexicographic string ordering
ok 4 - lexicographic string ordering
---
duration_ms: 0.045231
type: 'test'
...
# Subtest: ordering with missing field is false
ok 5 - ordering with missing field is false
---
duration_ms: 0.04051
type: 'test'
...
# Subtest: ordering with type mismatch is false (no throw)
ok 6 - ordering with type mismatch is false (no throw)
---
duration_ms: 0.03639
type: 'test'
...
# Subtest: eq with missing field is false, ne is true
not ok 7 - eq with missing field is false, ne is true
---
duration_ms: 1.024957
type: 'test'
location: '/tmp/llmlab-ts-10-rule-engine-dm93yw7o/solution.test.ts:1:2051'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly equal:
false !== true
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected: true
actual: false
operator: 'strictEqual'
stack: |-
TestContext.<anonymous> (/tmp/llmlab-ts-10-rule-engine-dm93yw7o/solution.test.ts:52:10)
Test.runInAsyncScope (node:async_hooks:227:14)
Test.run (node:internal/test_runner/test:1306:25)
Test.processPendingSubtests (node:internal/test_runner/test:897:18)
Test.postRun (node:internal/test_runner/test:1447:19)
Test.run (node:internal/test_runner/test:1372:12)
async Test.processPendingSubtests (node:internal/test_runner/test:897:7)
...
# Subtest: in membership
ok 8 - in membership
---
duration_ms: 0.08421
type: 'test'
...
# Subtest: not negates its child
ok 9 - not negates its child
---
duration_ms: 0.060001
type: 'test'
...
# Subtest: empty and is true, empty or is false
ok 10 - empty and is true, empty or is false
---
duration_ms: 0.084111
type: 'test'
...
# Subtest: and / or short-circuit semantics over multiple rules
ok 11 - and / or short-circuit semantics over multiple rules
---
duration_ms: 0.07825
type: 'test'
...
# Subtest: deeply nested and / or / not
ok 12 - deeply nested and / or / not
---
duration_ms: 0.049951
type: 'test'
...
# Subtest: filter keeps matching records in original order
ok 13 - filter keeps matching records in original order
---
duration_ms: 0.335892
type: 'test'
...
# Subtest: filter with empty-and matches everything; empty-or matches nothing
ok 14 - filter with empty-and matches everything; empty-or matches nothing
---
duration_ms: 0.06202
type: 'test'
...
# Subtest: RuleError thrown for an unknown op
ok 15 - RuleError thrown for an unknown op
---
duration_ms: 0.196241
type: 'test'
...
# Subtest: RuleError thrown for malformed and / not / comparison nodes
ok 16 - RuleError thrown for malformed and / not / comparison nodes
---
duration_ms: 0.115201
type: 'test'
...
# Subtest: filter propagates RuleError from an invalid rule
ok 17 - filter propagates RuleError from an invalid rule
---
duration_ms: 0.06175
type: 'test'
...
1..17
# tests 17
# suites 0
# pass 16
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 83.699166
Self-repair attempts
[
{
"total": 17,
"answer": "```ts\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 isNumberOrString(value: unknown): value is number | string {\n return typeof value === \"number\" || typeof value === \"string\";\n}\n\nfunction validateRule(rule: unknown): asserts rule is Rule {\n if (typeof rule !== \"object\" || rule === null) {\n throw new RuleError(\"Rule must be an object\");\n }\n\n const r = rule as Record<string, unknown>;\n if (!(\"op\" in r)) {\n throw new RuleError(\"Rule is missing 'op'\");\n }\n\n const op = r.op;\n if (typeof op !== \"string\") {\n throw new RuleError(\"Rule 'op' must be a string\");\n }\n\n switch (op) {\n case \"and\":\n case \"or\": {\n if (!(\"rules\" in r) || !Array.isArray(r.rules)) {\n throw new RuleError(`Rule with op '${op}' must have an array 'rules'`);\n }\n for (const subRule of r.rules) {\n validateRule(subRule);\n }\n break;\n }\n case \"not\": {\n if (!(\"rule\" in r)) {\n throw new RuleError(\"Rule with op 'not' must have a 'rule' child\");\n }\n validateRule(r.rule);\n break;\n }\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\": {\n if (!(\"field\" in r) || typeof r.field !== \"string\") {\n throw new RuleError(`Rule with op '${op}' must have a string 'field'`);\n }\n if (!(\"value\" in r) || !isNumberOrString(r.value)) {\n throw new RuleError(`Rule with op '${op}' must have a number or string 'value'`);\n }\n break;\n }\n case \"in\": {\n if (!(\"field\" in r) || typeof r.field !== \"string\") {\n throw new RuleError(\"Rule with op 'in' must have a string 'field'\");\n }\n if (!(\"values\" in r) || !Array.isArray(r.values)) {\n throw new RuleError(\"Rule with op 'in' must have an array 'values'\");\n }\n for (const v of r.values) {\n if (!isNumberOrString(v)) {\n throw new RuleError(\"Rule with op 'in' must have values that are numbers or strings\");\n }\n }\n break;\n }\n default: {\n throw new RuleError(`Unknown rule operator '${op}'`);\n }\n }\n}\n\nfunction evaluateComparison(\n op: \"eq\" | \"ne\" | \"lt\" | \"lte\" | \"gt\" | \"gte\",\n fieldValue: unknown,\n value: number | string\n): boolean {\n if (fieldValue === undefined) {\n return false;\n }\n\n if (op === \"eq\") {\n return fieldValue === value;\n }\n if (op === \"ne\") {\n return fieldValue !== value;\n }\n\n // For ordering comparisons, ensure both sides are of the same comparable type\n if (typeof fieldValue === \"number\" && typeof value === \"number\") {\n switch (op) {\n case \"lt\":\n return fieldValue < value;\n case \"lte\":\n return fieldValue <= value;\n case \"gt\":\n return fieldValue > value;\n case \"gte\":\n return fieldValue >= value;\n default:\n return false;\n }\n }\n\n if (typeof fieldValue === \"string\" && typeof value === \"string\") {\n switch (op) {\n case \"lt\":\n return fieldValue < value;\n case \"lte\":\n return fieldValue <= value;\n case \"gt\":\n return fieldValue > value;\n case \"gte\":\n return fieldValue >= value;\n default:\n return false;\n }\n }\n\n return false;\n}\n\nexport function evaluate(rule: Rule, record: Record<string, unknown>): boolean {\n validateRule(rule);\n\n switch (rule.op) {\n case \"and\": {\n for (const subRule of rule.rules) {\n if (!evaluate(subRule, record)) {\n return false;\n }\n }\n return true;\n }\n case \"or\": {\n for (const subRule of rule.rules) {\n if (evaluate(subRule, record)) {\n return true;\n }\n }\n return false;\n }\n case \"not\": {\n return !evaluate(rule.rule, record);\n }\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\": {\n const fieldValue = record[rule.field];\n return evaluateComparison(rule.op, fieldValue, rule.value);\n }\n case \"in\": {\n const fieldValue = record[rule.field];\n return rule.values.some((v) => v === fieldValue);\n }\n default: {\n // Should not reach here due to validation\n throw new RuleError(`Unknown rule operator '${rule.op}'`);\n }\n }\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": 16,
"test_error": " ok 7 - eq with missing field is false, ne is true\n ---\n duration_ms: 1.024957\n type: 'test'\n location: '/tmp/llmlab-ts-10-rule-engine-dm93yw7o/solution.test.ts:1:2051'\n failureType: 'testCodeFailure'\n error: |-\n Expected values to be strictly equal:\n \n false !== true\n \n code: 'ERR_ASSERTION'\n name: 'AssertionError'\n expected: true\n actual: false\n operator: 'strictEqual'\n stack: |-\n TestContext.<anonymous> (/tmp/llmlab-ts-10-rule-engine-dm93yw7o/solution.test.ts:52:10)\n Test.runInAsyncScope (node:async_hooks:227:14)\n Test.run (node:internal/test_runner/test:1306:25)\n Test.processPendingSubtests (node:internal/test_runner/test:897:18)\n Test.postRun (node:internal/test_runner/test:1447:19)\n Test.run (node:internal/test_runner/test:1372:12)\n async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n ...\n# Subtest: in membership\nok 8 - in membership\n ---\n duration_ms: 0.08421\n type: 'test'\n ...\n# Subtest: not negates its child\nok 9 - not negates its child\n ---\n duration_ms: 0.060001\n type: 'test'\n ...\n# Subtest: empty and is true, empty or is false\nok 10 - empty and is true, empty or is false\n ---\n duration_ms: 0.084111\n type: 'test'\n ...\n# Subtest: and / or short-circuit semantics over multiple rules\nok 11 - and / or short-circuit semantics over multiple rules\n ---\n duration_ms: 0.07825\n type: 'test'\n ...\n# Subtest: deeply nested and / or / not\nok 12 - deeply nested and / or / not\n ---\n duration_ms: 0.049951\n type: 'test'\n ...\n# Subtest: filter keeps matching records in original order\nok 13 - filter keeps matching records in original order\n ---\n duration_ms: 0.335892\n type: 'test'\n ...\n# Subtest: filter with empty-and matches everything; empty-or matches nothing\nok 14 - filter with empty-and matches everything; empty-or matches nothing\n ---\n duration_ms: 0.06202\n type: 'test'\n ...\n# Subtest: RuleError thrown for an unknown op\nok 15 - RuleError thrown for an unknown op\n ---\n duration_ms: 0.196241\n type: 'test'\n ...\n# Subtest: RuleError thrown for malformed and / not / comparison nodes\nok 16 - RuleError thrown for malformed and / not / comparison nodes\n ---\n duration_ms: 0.115201\n type: 'test'\n ...\n# Subtest: filter propagates RuleError from an invalid rule\nok 17 - filter propagates RuleError from an invalid rule\n ---\n duration_ms: 0.06175\n type: 'test'\n ...\n1..17\n# tests 17\n# suites 0\n# pass 16\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 83.699166"
},
{
"total": 17,
"answer": "```ts\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 isNumberOrString(value: unknown): value is number | string {\n return typeof value === \"number\" || typeof value === \"string\";\n}\n\nfunction validateRule(rule: unknown): asserts rule is Rule {\n if (typeof rule !== \"object\" || rule === null) {\n throw new RuleError(\"Rule must be an object\");\n }\n\n const r = rule as Record<string, unknown>;\n if (!(\"op\" in r)) {\n throw new RuleError(\"Rule is missing 'op'\");\n }\n\n const op = r.op;\n if (typeof op !== \"string\") {\n throw new RuleError(\"Rule 'op' must be a string\");\n }\n\n switch (op) {\n case \"and\":\n case \"or\": {\n if (!(\"rules\" in r) || !Array.isArray(r.rules)) {\n throw new RuleError(`Rule with op '${op}' must have an array 'rules'`);\n }\n for (const subRule of r.rules) {\n validateRule(subRule);\n }\n break;\n }\n case \"not\": {\n if (!(\"rule\" in r)) {\n throw new RuleError(\"Rule with op 'not' must have a 'rule' child\");\n }\n validateRule(r.rule);\n break;\n }\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\": {\n if (!(\"field\" in r) || typeof r.field !== \"string\") {\n throw new RuleError(`Rule with op '${op}' must have a string 'field'`);\n }\n if (!(\"value\" in r) || !isNumberOrString(r.value)) {\n throw new RuleError(`Rule with op '${op}' must have a number or string 'value'`);\n }\n break;\n }\n case \"in\": {\n if (!(\"field\" in r) || typeof r.field !== \"string\") {\n throw new RuleError(\"Rule with op 'in' must have a string 'field'\");\n }\n if (!(\"values\" in r) || !Array.isArray(r.values)) {\n throw new RuleError(\"Rule with op 'in' must have an array 'values'\");\n }\n for (const v of r.values) {\n if (!isNumberOrString(v)) {\n throw new RuleError(\"Rule with op 'in' must have values that are numbers or strings\");\n }\n }\n break;\n }\n default: {\n throw new RuleError(`Unknown rule operator '${op}'`);\n }\n }\n}\n\nfunction evaluateComparison(\n op: \"eq\" | \"ne\" | \"lt\" | \"lte\" | \"gt\" | \"gte\",\n fieldValue: unknown,\n value: number | string\n): boolean {\n if (op === \"eq\") {\n return fieldValue === value;\n }\n if (op === \"ne\") {\n return fieldValue !== value;\n }\n\n // For ordering comparisons, ensure both sides are of the same comparable type\n if (typeof fieldValue === \"number\" && typeof value === \"number\") {\n switch (op) {\n case \"lt\":\n return fieldValue < value;\n case \"lte\":\n return fieldValue <= value;\n case \"gt\":\n return fieldValue > value;\n case \"gte\":\n return fieldValue >= value;\n default:\n return false;\n }\n }\n\n if (typeof fieldValue === \"string\" && typeof value === \"string\") {\n switch (op) {\n case \"lt\":\n return fieldValue < value;\n case \"lte\":\n return fieldValue <= value;\n case \"gt\":\n return fieldValue > value;\n case \"gte\":\n return fieldValue >= value;\n default:\n return false;\n }\n }\n\n return false;\n}\n\nexport function evaluate(rule: Rule, record: Record<string, unknown>): boolean {\n validateRule(rule);\n\n switch (rule.op) {\n case \"and\": {\n for (const subRule of rule.rules) {\n if (!evaluate(subRule, record)) {\n return false;\n }\n }\n return true;\n }\n case \"or\": {\n for (const subRule of rule.rules) {\n if (evaluate(subRule, record)) {\n return true;\n }\n }\n return false;\n }\n case \"not\": {\n return !evaluate(rule.rule, record);\n }\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\": {\n const fieldValue = record[rule.field];\n return evaluateComparison(rule.op, fieldValue, rule.value);\n }\n case \"in\": {\n const fieldValue = record[rule.field];\n return rule.values.some((v) => v === fieldValue);\n }\n default: {\n // Should not reach here due to validation\n throw new RuleError(`Unknown rule operator '${rule.op}'`);\n }\n }\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": ""
}
]