{"_id":"onesixty","_rev":"7-47d7972a8b8803f65acef94fa578c7e2","name":"onesixty","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"onesixty","version":"0.1.0","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"author":{"name":"lily","email":"oss@colonthr.ee"},"license":"Apache-2.0","_id":"onesixty@0.1.0","maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"homepage":"https://github.com/repository/onesixty#readme","bugs":{"url":"https://github.com/repository/onesixty/issues"},"dist":{"shasum":"d0dfa53969f5c6e5189ad636208bf10563db4b0f","tarball":"https://registry.npmjs.org/onesixty/-/onesixty-0.1.0.tgz","fileCount":5,"integrity":"sha512-ZcXvSZC9+kGal95vvjY9sB36cfXgakPs9oPL9Yct+ySdpH2PFca1tq8H1T+elS4TQOEwqOleBF5oIuUQOnzY4g==","signatures":[{"sig":"MEUCIH5GEQLzs4BglbRa3FhofBCF4fQJQrA3g4g197haHNuEAiEAlof+0VpXuRfyXP4SLIKvAFwl6FTVD80nk6+KkmsmL2w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":108450},"pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"9e5273749950eb516abe03fbcc2d5b3a4d4e1261","scripts":{"dev":"tsdown --watch","fmt":"oxfmt .","lint":"oxlint","test":"vitest run","bench":"vitest bench","build":"tsdown","release":"bumpp","lint:fix":"oxlint --fix","api:check":"tsdown && diff dist/index.d.mts api/index.d.mts","fmt:check":"oxfmt --check .","typecheck":"tsgo --noEmit","playground":"tsx scripts/playground.ts","api:extract":"tsdown && cp dist/index.d.mts api/index.d.mts","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:13e0b551-5ee6-432d-ba92-01610324a27d"}},"repository":{"url":"git+https://github.com/repository/onesixty.git","type":"git"},"_npmVersion":"11.9.0","description":"AIP-160 filtering language parser and evaluator for TypeScript","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsx":"^4.21.0","bumpp":"^11.0.0","oxfmt":"^0.41.0","oxlint":"^1.56.0","tsdown":"^0.21.4","vitest":"^4.1.0","typescript":"^5.9.3","@types/node":"^25.5.0","oxlint-tsgolint":"^0.17.0","@typescript/native-preview":"7.0.0-dev.20260315.1"},"_npmOperationalInternal":{"tmp":"tmp/onesixty_0.1.0_1773713713463_0.8045484392919917","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"onesixty","version":"0.1.1","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"author":{"name":"lily","email":"oss@colonthr.ee"},"license":"Apache-2.0","_id":"onesixty@0.1.1","maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"homepage":"https://github.com/repository/onesixty#readme","bugs":{"url":"https://github.com/repository/onesixty/issues"},"dist":{"shasum":"befedc896d2d05a5b59bd10cc58e48a0ae01b8b1","tarball":"https://registry.npmjs.org/onesixty/-/onesixty-0.1.1.tgz","fileCount":5,"integrity":"sha512-+CqPB9KcAGWtrdNYrpQ7CjRDRL0XHQrUyXeFmKgqkF1N8mtrSK70SIZCCZRx1UmuyWb1Zi186CgkT/uu44mkVA==","signatures":[{"sig":"MEUCIQC4Nmym6lrpeD0Cb3BdeeFVGtrNicloEz3ES8Pw4D/v1QIgOCzHvCRQ8/lELb31muMQZ1GUpPyJmbsfyU8QnCOZmLc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/onesixty@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":108589},"pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"6b7ca410246ac1dc3e77a1ed44486f9ceeb7f8b8","scripts":{"dev":"tsdown --watch","fmt":"oxfmt .","lint":"oxlint","test":"vitest run","bench":"vitest bench","build":"tsdown","release":"bumpp","lint:fix":"oxlint --fix","api:check":"tsdown && diff dist/index.d.mts api/index.d.mts","fmt:check":"oxfmt --check .","typecheck":"tsgo --noEmit","playground":"tsx scripts/playground.ts","api:extract":"tsdown && cp dist/index.d.mts api/index.d.mts","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:13e0b551-5ee6-432d-ba92-01610324a27d"}},"repository":{"url":"git+https://github.com/repository/onesixty.git","type":"git"},"_npmVersion":"11.9.0","description":"AIP-160 filtering language parser and evaluator for TypeScript","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsx":"^4.21.0","bumpp":"^11.0.0","oxfmt":"^0.41.0","oxlint":"^1.56.0","tsdown":"^0.21.4","vitest":"^4.1.0","typescript":"^5.9.3","@types/node":"^25.5.0","oxlint-tsgolint":"^0.17.0","@typescript/native-preview":"7.0.0-dev.20260315.1"},"_npmOperationalInternal":{"tmp":"tmp/onesixty_0.1.1_1773714850514_0.3617889621702035","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"onesixty","version":"0.1.2","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"author":{"name":"lily","email":"oss@colonthr.ee"},"license":"Apache-2.0","_id":"onesixty@0.1.2","maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"homepage":"https://github.com/repository/onesixty#readme","bugs":{"url":"https://github.com/repository/onesixty/issues"},"dist":{"shasum":"9bcf45d78a1a21079b3fa52b5df4b8e5f0871130","tarball":"https://registry.npmjs.org/onesixty/-/onesixty-0.1.2.tgz","fileCount":5,"integrity":"sha512-5b6XHE/SnDWqh64x93cpz0PjSoAkWhIfeAIx1fmA0YqoUUTlWZL4yPDx8pJQEg6LMqfH9Et/9sQinUQirasxwA==","signatures":[{"sig":"MEUCIQCyqHHRJBfsf/oG0LTkkVKnN6cyN+MNir6COcFL2k78fgIgOfkcKlpyQzS2kwAkinb9+lkq/aLQUxiwdP41cSxLNqo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/onesixty@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":108589},"pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"7dbb649cd515af05a381d430f07a245c8131cb95","scripts":{"dev":"tsdown --watch","fmt":"oxfmt .","lint":"oxlint","test":"vitest run","bench":"vitest bench","build":"tsdown","release":"bumpp","lint:fix":"oxlint --fix","api:check":"tsdown && diff dist/index.d.mts api/index.d.mts","fmt:check":"oxfmt --check .","typecheck":"tsgo --noEmit","playground":"tsx scripts/playground.ts","api:extract":"tsdown && cp dist/index.d.mts api/index.d.mts","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:13e0b551-5ee6-432d-ba92-01610324a27d"}},"repository":{"url":"git+https://github.com/repository/onesixty.git","type":"git"},"_npmVersion":"11.9.0","description":"AIP-160 filtering language parser and evaluator for TypeScript","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsx":"^4.21.0","bumpp":"^11.0.0","oxfmt":"^0.41.0","oxlint":"^1.56.0","tsdown":"^0.21.4","vitest":"^4.1.0","typescript":"^5.9.3","@types/node":"^25.5.0","oxlint-tsgolint":"^0.17.0","@typescript/native-preview":"7.0.0-dev.20260315.1"},"_npmOperationalInternal":{"tmp":"tmp/onesixty_0.1.2_1773715598970_0.06315045860876856","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"onesixty","version":"0.1.3","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"author":{"name":"lily","email":"oss@colonthr.ee"},"license":"Apache-2.0","_id":"onesixty@0.1.3","maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"homepage":"https://github.com/repository/onesixty#readme","bugs":{"url":"https://github.com/repository/onesixty/issues"},"dist":{"shasum":"05633a631f46e6ff31c05c27e07bfad3dd235b1a","tarball":"https://registry.npmjs.org/onesixty/-/onesixty-0.1.3.tgz","fileCount":5,"integrity":"sha512-RXBxMy5rAGiKB0L9ZQYcjOMRFWOBb70P+WO+1f7sS+iHqxogVzSateQd9ifSyyVYHOz1fWT4wLnerhu7JC6W7Q==","signatures":[{"sig":"MEQCIDYilO5KyNNO3cAos9jdgnHM4iazDUJXUO/u+2esUGbYAiAM847YdBv+u/Gl6AqBqagahr7IeEv/Gy2Sw1r1NhWi5Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/onesixty@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":108806},"pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"d8dc552c06fc125e6a2f94afb57ccdc5bf92b368","scripts":{"dev":"tsdown --watch","fmt":"oxfmt .","lint":"oxlint","test":"vitest run","bench":"vitest bench","build":"tsdown","release":"bumpp","lint:fix":"oxlint --fix","api:check":"tsdown && diff dist/index.d.mts api/index.d.mts","fmt:check":"oxfmt --check .","typecheck":"tsgo --noEmit","playground":"tsx scripts/playground.ts","api:extract":"tsdown && cp dist/index.d.mts api/index.d.mts","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:13e0b551-5ee6-432d-ba92-01610324a27d"}},"repository":{"url":"git+https://github.com/repository/onesixty.git","type":"git"},"_npmVersion":"11.9.0","description":"AIP-160 filtering language parser and evaluator for TypeScript","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsx":"^4.21.0","bumpp":"^11.0.0","oxfmt":"^0.41.0","oxlint":"^1.56.0","tsdown":"^0.21.4","vitest":"^4.1.0","typescript":"^5.9.3","@types/node":"^25.5.0","oxlint-tsgolint":"^0.17.0","@typescript/native-preview":"7.0.0-dev.20260315.1"},"_npmOperationalInternal":{"tmp":"tmp/onesixty_0.1.3_1773761522615_0.47723472798459654","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"onesixty","version":"0.2.0","description":"AIP-160 filtering language parser and evaluator for TypeScript","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"homepage":"https://github.com/repository/onesixty#readme","bugs":{"url":"https://github.com/repository/onesixty/issues"},"license":"Apache-2.0","author":{"name":"lily","email":"oss@colonthr.ee"},"repository":{"type":"git","url":"git+https://github.com/repository/onesixty.git"},"type":"module","sideEffects":false,"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./package.json":"./package.json"},"scripts":{"build":"tsdown","dev":"tsdown --watch","test":"vitest run","bench":"vitest bench","typecheck":"tsgo --noEmit","lint":"oxlint","lint:fix":"oxlint --fix","fmt":"oxfmt .","fmt:check":"oxfmt --check .","api:extract":"tsdown && cp dist/index.d.mts api/index.d.mts","api:check":"tsdown && diff dist/index.d.mts api/index.d.mts","playground":"tsx scripts/playground.ts","release":"bumpp","prepublishOnly":"pnpm run build"},"devDependencies":{"@tcn/aip-160":"^1.2.5","@types/node":"^25.5.0","@typescript/native-preview":"7.0.0-dev.20260315.1","bumpp":"^11.0.0","oxfmt":"^0.41.0","oxlint":"^1.56.0","oxlint-tsgolint":"^0.17.0","tsdown":"^0.21.4","tsx":"^4.21.0","typescript":"^5.9.3","vitest":"^4.1.0"},"packageManager":"pnpm@10.32.1","pnpm":{"onlyBuiltDependencies":["esbuild"]},"gitHead":"1038037321a1aec4be8ab463e134bd1a3604c02c","_id":"onesixty@0.2.0","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-MsGR1XB1ElaqMYW6mMGG7r67a+rZ8HmKpebYkyid5clKh/3dQZ3H0bhMFJT5i54ua35iIPCx4Pgi4yTu99lV4w==","shasum":"4b5879667b6062fdd009a603e0dcba7f3818f6ac","tarball":"https://registry.npmjs.org/onesixty/-/onesixty-0.2.0.tgz","fileCount":5,"unpackedSize":126405,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/onesixty@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCECs47bnNdXnSexmacOZDAnC9WQfKRTa3Shw7Unq7UTAIhAKyajwIlE73/caYZvbZ5kwW2rlJbB6DOHKkArFZ9u6AQ"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:13e0b551-5ee6-432d-ba92-01610324a27d"}},"directories":{},"maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/onesixty_0.2.0_1773849511956_0.4027018426507727"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-26T20:18:59.417Z","modified":"2026-03-18T15:58:32.383Z","1.0.0":"2026-02-26T20:18:59.572Z","0.1.0":"2026-03-17T02:15:13.635Z","0.1.1":"2026-03-17T02:34:10.674Z","0.1.2":"2026-03-17T02:46:39.116Z","0.1.3":"2026-03-17T15:32:02.803Z","0.2.0":"2026-03-18T15:58:32.111Z"},"bugs":{"url":"https://github.com/repository/onesixty/issues"},"author":{"name":"lily","email":"oss@colonthr.ee"},"license":"Apache-2.0","homepage":"https://github.com/repository/onesixty#readme","keywords":["aip-160","api","evaluator","filter","filtering","google","list-filter","parser","query"],"repository":{"type":"git","url":"git+https://github.com/repository/onesixty.git"},"description":"AIP-160 filtering language parser and evaluator for TypeScript","maintainers":[{"name":"lilyc3","email":"oss@colonthr.ee"}],"readme":"# onesixty\n\nFilter objects with expressive, user-facing query strings.\n\n```ts\nimport { filter } from \"onesixty\";\n\nfilter('role = \"magical_girl\" AND power >= 3', { role: \"magical_girl\", power: 5 }); // true\n```\n\nonesixty is a zero-dependency TypeScript implementation of [AIP-160](https://google.aip.dev/160), the filtering language used across Google APIs. Parse filter expressions into a type-safe AST, evaluate them against plain objects, or compile them into your own backend (SQL, Elasticsearch, etc.).\n\n- **Zero dependencies,** 12kB gzipped. Just TypeScript, nothing else\n- **Fast.** 1M+ filter evaluations per second, 10-15x faster than alternatives\n- **Full AIP-160 grammar:** comparisons, `AND`/`OR`/`NOT`, field traversal, `:` (has), functions, wildcards, parentheses\n- **Compile once, run many:** parse a filter once, evaluate it against thousands of objects\n- **Async functions:** custom functions can return promises\n- **Serializable:** compiled filters survive `JSON.stringify` for storage and transfer\n- **Structured errors:** every error is a typed class with machine-readable data, not just a message string\n- **Tolerant mode:** collect all errors and get a best-effort CST for editor integrations and as-you-type validation\n- **Bring your own backend:** use the AST directly to generate SQL, Elasticsearch queries, or anything else\n\n## Install\n\n```bash\nnpm install onesixty\npnpm add onesixty\nyarn add onesixty\nbun add onesixty\n```\n\nTry it interactively: `pnpm playground`\n\n## Usage\n\n### One-shot filtering\n\n```ts\nimport { filter } from \"onesixty\";\n\nconst girls = [\n  { name: \"Mami\", rank: 5, role: \"magical_girl\" },\n  { name: \"Sayaka\", rank: 2, role: \"civilian\" },\n  { name: \"Kyoko\", rank: 4, role: \"magical_girl\" },\n];\n\ngirls.filter((g) => filter('role = \"magical_girl\" AND rank >= 4', g));\n// [Mami, Kyoko]\n```\n\n### Compile once, evaluate many\n\n```ts\nimport { compile } from \"onesixty\";\n\nconst f = compile('status = \"contracted\" AND power >= 3');\n\nfor (const item of items) {\n  if (f.evaluate(item)) {\n    // matched\n  }\n}\n```\n\n### Custom functions\n\n```ts\nfilter(\"distance(lat, lng) < 100\", coords, {\n  functions: {\n    distance: (lat, lng) => haversine(lat, lng, userLat, userLng),\n  },\n});\n```\n\nAsync functions work too. Use `filterAsync` or `compile().evaluateAsync()`:\n\n```ts\n// Check if the current request is authorized for a resource\nconst f = compile(\"authorized(resource)\");\n\nawait f.evaluateAsync(request, {\n  authorized: async (resource) => {\n    const perms = await db.getPermissions(request.user);\n    return perms.includes(resource);\n  },\n});\n```\n\n### Serialization\n\nCompiled filters can be persisted (e.g. to a database) and restored without re-parsing:\n\n```ts\nimport { compile, CompiledFilter } from \"onesixty\";\n\n// Save\nconst json = JSON.stringify(compile('status = \"contracted\"').toSerialized());\n\n// Restore\nconst f = CompiledFilter.fromSerialized(JSON.parse(json));\nf.evaluate({ status: \"contracted\" }); // true\n```\n\n### Pipeline API\n\nFor advanced use cases, the full parse-transform-evaluate pipeline is exposed as separate functions:\n\n```ts\nimport { parse, transform, evaluate, type ASTNode } from \"onesixty\";\n\n// Parse and transform in two steps: string -> CST -> AST\nconst ast = transform(parse('status = \"contracted\" AND grief <= 50'));\n\n// Evaluate directly against an object\nevaluate(ast, { status: \"contracted\", grief: 30 }); // true\n```\n\nYou can also skip the built-in evaluator entirely and handle evaluation through your own means with the AST.\n\n```ts\n// Walk the AST to build a WHERE clause\nconst params: string[] = [];\nfunction toSQL(node: ASTNode | null): string {\n  if (!node) return \"1=1\";\n  if (node.type === \"and\") return node.children.map(toSQL).join(\" AND \");\n  if (node.type === \"not\") return `NOT (${toSQL(node.child)})`;\n  if (node.type === \"restriction\" && node.comparable.type === \"member\") {\n    params.push(node.arg?.type === \"value\" ? node.arg.value : \"\");\n    return `${node.comparable.path.join(\".\")} ${node.comparator} $${params.length}`;\n  }\n  return \"1=1\";\n}\n\ntoSQL(ast); // \"status = $1 AND grief <= $2\", params: [\"contracted\", \"50\"]\n```\n\n### Tolerant parsing\n\nBy default, `parse` throws on the first syntax error. Pass `tolerant: true` to collect all errors and get a best-effort CST instead. This is useful for editor integrations, as-you-type validation, and anywhere you want diagnostics without aborting.\n\n```ts\nimport { parse, toCleanTree, transform } from \"onesixty\";\n\nconst result = parse(\"status = AND power >= 3\", { tolerant: true });\n\nresult.ok; // false - there are errors\nresult.errors; // [ExpectedValueError: Expected a value after '=', found 'AND']\nresult.cst; // complete CST - 'status = <placeholder>' AND 'power >= 3'\n\n// If the tree is clean, narrow it to a strict FilterNode for evaluation\nconst clean = toCleanTree(result);\nif (clean) {\n  const ast = transform(clean);\n}\n```\n\nThe tolerant parser never throws. It uses insertion-based recovery to fill in missing values with zero-width placeholders, so subsequent valid expressions are still parsed. `toCleanTree` returns `null` if any errors were found, or a strict `FilterNode` you can pass to `transform`.\n\n### Error handling\n\nAll errors are typed classes with structured data. Catch them broadly or narrowly:\n\n```ts\nimport { filter, FilterError } from \"onesixty\";\n\ntry {\n  filter(\"a AND AND b\", {});\n} catch (e) {\n  if (e instanceof FilterError) {\n    e.description; // \"Expected an expression after 'AND', found keyword 'AND'\"\n    e.span; // { start: 6, end: 9 }\n    e.hints; // [\"Remove the duplicate 'AND', ...\"]\n  }\n}\n```\n\nEvery error subclass exposes the relevant tokens, positions, and context as typed readonly fields. See the JSDoc on each error class for details.\n\n---\n\n## Benchmarks\n\nMeasured on a MacBook M4 Pro with Node.js 24, using `vitest bench`. All numbers are operations per second (higher is better). The comparison target is [`@tcn/aip-160`](https://www.npmjs.com/package/@tcn/aip-160), the other AIP-160 implementation on npm.\n\n### End-to-end: parse + evaluate\n\n| Expression                              |  onesixty | @tcn/aip-160 | Ratio |\n| --------------------------------------- | --------: | -----------: | ----: |\n| `a = 1`                                 | 2,285,479 |      149,225 |   15x |\n| 4 restrictions with AND, has, traversal |   572,782 |       43,775 |   13x |\n| OR + nested path + NOT + wildcard       |   352,348 |       35,659 |   10x |\n| Global text search on nested object     | 1,650,356 |      335,975 |    5x |\n\n### Compile once, evaluate many (x100 loop)\n\n| Approach                   | onesixty | @tcn/aip-160 | Ratio |\n| -------------------------- | -------: | -----------: | ----: |\n| `compile()` + `evaluate()` |  144,462 |          n/a |   n/a |\n| `filter()` (re-parse each) |   10,464 |          670 |   16x |\n\n### Stress tests\n\n| Scenario                               |  onesixty | @tcn/aip-160 | Ratio |\n| -------------------------------------- | --------: | -----------: | ----: |\n| 50 chained AND restrictions            |    42,447 |        2,461 |   17x |\n| 32 levels of parentheses               |   131,251 |       10,538 |   12x |\n| Last key in 1,000-key object           | 1,962,401 |      113,214 |   17x |\n| Global search miss on 1,000-key object |    93,253 |      118,047 |  0.8x |\n| Array fanout: 1,000 elements           |    20,984 |          n/a |   n/a |\n\n### Pipeline stages (onesixty internals)\n\n| Stage                       |    ops/sec |\n| --------------------------- | ---------: |\n| tokenize                    |  4,794,457 |\n| parse                       |  1,212,612 |\n| parse + transform           |  1,172,134 |\n| evaluate (pre-compiled AST) | 12,154,248 |\n| filter (end-to-end)         |  1,034,112 |\n\nRun the benchmarks yourself with `pnpm bench`.\n\n---\n\n## Reference\n\n### Filter syntax\n\nonesixty implements the full [AIP-160](https://google.aip.dev/160) grammar. See the spec for the complete language reference.\n\n<details>\n<summary>Syntax cheat sheet</summary>\n\n| Feature          | Example                             | Notes                                            |\n| ---------------- | ----------------------------------- | ------------------------------------------------ |\n| Comparisons      | `power >= 3`, `name = \"Madoka\"`     | `=`, `!=`, `<`, `<=`, `>`, `>=`                  |\n| Has (membership) | `abilities:magic`                   | Array contains value, map has key                |\n| Presence         | `field:*`                           | Field is present and non-empty                   |\n| AND              | `a = 1 AND b = 2`                   | Both must match                                  |\n| OR               | `a = 1 OR a = 2`                    | Either must match                                |\n| NOT              | `NOT status = \"witched\"`            | Negation (`-` shorthand: `-file:\".java\"`)        |\n| Implicit AND     | `Homura Madoka`                     | Whitespace-separated = AND-joined                |\n| Traversal        | `user.soul_gem.city = \"Mitakihara\"` | Dot-separated field paths                        |\n| Functions        | `cohort(request.user)`              | Custom functions, qualified names (`math.abs()`) |\n| Parentheses      | `(a OR b) AND c`                    | Grouping and precedence override                 |\n| Wildcards        | `name = \"Mami-*\"`                   | Only in quoted strings with `=`                  |\n\n**Precedence:** OR binds tighter than AND. `a AND b OR c` means `a AND (b OR c)`.\n\n</details>\n\n### AST node types\n\nThe AST is a discriminated union on the `type` field. Every node is a plain, JSON-serializable object.\n\n<details>\n<summary>Node type reference</summary>\n\n| Type                 | `type`          | Key fields                                                   | Description                                    |\n| -------------------- | --------------- | ------------------------------------------------------------ | ---------------------------------------------- |\n| `AndNode`            | `\"and\"`         | `children: ASTNode[]`                                        | Logical AND (2+ children, flattened)           |\n| `OrNode`             | `\"or\"`          | `children: ASTNode[]`                                        | Logical OR (2+ children, flattened)            |\n| `NotNode`            | `\"not\"`         | `child: ASTNode`                                             | Logical negation                               |\n| `ASTRestrictionNode` | `\"restriction\"` | `comparable`, `comparator`, `arg`                            | A comparison (`power >= 3`, `abilities:magic`) |\n| `GlobalNode`         | `\"global\"`      | `value`                                                      | A bare value for text search (`Madoka`)        |\n| `ASTValueNode`       | `\"value\"`       | `value: string`, `quoted: boolean`                           | A literal string or text value                 |\n| `ASTMemberNode`      | `\"member\"`      | `path: string[]`                                             | A dotted field path                            |\n| `ASTFunctionNode`    | `\"function\"`    | `name: string[]`, `qualifiedName: string`, `args: ASTNode[]` | A function call                                |\n\nThe `comparable` field on restrictions is always `ASTMemberNode | ASTFunctionNode`. The `comparator` is one of `\"=\" | \"!=\" | \"<\" | \"<=\" | \">\" | \">=\" | \":\"`. Every node carries a `span: { start, end }` pointing back to the original source string.\n\n</details>\n\n### Options\n\n<details>\n<summary>Parse options</summary>\n\n| Option      | Type      | Default | Description                                                           |\n| ----------- | --------- | ------- | --------------------------------------------------------------------- |\n| `maxDepth`  | `number`  | `64`    | Maximum parenthesis nesting depth                                     |\n| `maxLength` | `number`  | `8192`  | Maximum input string length                                           |\n| `tolerant`  | `boolean` | `false` | Collect errors instead of throwing; return type becomes `ParseResult` |\n| `maxErrors` | `number`  | `20`    | Stop recovery after this many errors (only with `tolerant`)           |\n\n</details>\n\n<details>\n<summary>Evaluate options</summary>\n\n| Option               | Type                       | Default   | Description                                            |\n| -------------------- | -------------------------- | --------- | ------------------------------------------------------ |\n| `functions`          | `Record<string, Function>` |           | Custom function implementations                        |\n| `unknownFunction`    | `\"throw\" \\| \"false\"`       | `\"throw\"` | How to handle unregistered functions                   |\n| `globalSearchFields` | `string[]`                 |           | Limit bare-value search to these fields                |\n| `resolveRhsMembers`  | `boolean`                  | `false`   | Resolve dotted RHS paths against the target            |\n| `wildcardNotEquals`  | `boolean`                  | `false`   | Enable wildcards for `!=`                              |\n| `maxTraversalDepth`  | `number`                   | `32`      | Max recursion depth for global search and array fanout |\n\nSee the JSDoc on `EvaluateOptions` for full details on each option.\n\n</details>\n\n## License\n\n[Apache-2.0](./LICENSE)\n\nPortions of this project are modifications based on work created and shared by [Google](https://google.aip.dev/licensing) and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/).\n","readmeFilename":"README.md"}