bonsai-js 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-06-02
11
+
12
+ ### Security
13
+
14
+ - Bounded parser recursion: pathologically nested input (deep parentheses, unary chains) now fails closed with a typed `ExpressionError` ("Maximum expression nesting depth exceeded") instead of overflowing the native call stack with an uncaught `RangeError`. `validate()` reports it as a normal syntax error rather than a stack-overflow message.
15
+ - `maxArrayLength` is now enforced on array-returning methods (`split`, `map`, `flatMap`, `flat`, `concat`, `toSorted`, `toReversed`, `with`, `toSpliced`), not just array literals and spread, in both the sync and async evaluators.
16
+ - Added `maxStringLength` (default 100,000) and enforced it as a ceiling on every string produced by a method call, in both the sync and async evaluators. `padStart`/`padEnd`/`repeat` are checked before allocation (capping the produced length, not just the repeat count); all other string-returning methods (`join`, `concat`, `slice`, `toUpperCase`, ...) are checked on their output. This closes single-operation amplifiers such as `arr.join(sep)`, whose output length is array length times separator length in one native call that the cooperative timeout cannot interrupt.
17
+
18
+ ### Added
19
+
20
+ - `maxStringLength` option on `bonsai(options)` and a `BonsaiSecurityError('MAX_STRING_LENGTH', ...)` error code.
21
+ - `bonsai(options)` now validates its options and throws a `RangeError`/`TypeError` at construction for out-of-range values (negative `cacheSize`, non-positive `maxDepth`, negative `maxArrayLength`/`maxStringLength`, negative or non-finite `timeout`, allow/deny lists that are not arrays of strings). Previously a negative `cacheSize` silently disabled caching and other bad values failed later with confusing errors.
22
+
23
+ ### Fixed
24
+
25
+ - Async/sync parity for higher-order array methods (`map`, `filter`, `find`, `some`, `every`, `findIndex`, `flatMap`). Predicates now evaluate sequentially in the async evaluator, matching the synchronous one: `some`/`every`/`find`/`findIndex` short-circuit at the first decisive element, and `maxDepth` is no longer inflated by array length (an expression that succeeded with `evaluateSync` could previously throw a spurious `Maximum expression depth` error with `evaluate`).
26
+ - Malformed numeric literals now raise a parse-time `ExpressionError` instead of silently evaluating to `NaN` or coercing: a base prefix or exponent with no digits (`1e`, `1e+`, `0x`, `0b`, `0o`), and misplaced numeric separators (`1_`, `1__0`, `0xff_`), which are rejected the way JavaScript rejects them.
27
+ - Template interpolations containing string literals or nested template literals with braces (for example `` `${ "x}" }` `` or `` `${ `a}b` }` ``) now parse correctly instead of failing with an "Unterminated string"/"Unterminated template literal" error.
28
+ - Higher-order array helpers now fail loud on a non-function callback instead of silently doing the wrong thing. The lambda shorthand (`.x`, `. > 0`) is a function value; passing it to a function (for example `map(inc(.x))`) evaluates to a plain value, which previously made the stdlib `map`/`filter`/`find`/`some`/`every` transforms silently return the input unchanged and the `.map`/`.filter`/`.find`/`.findIndex`/`.some`/`.every`/`.flatMap` methods throw a raw `TypeError`. Both now throw a typed `BonsaiTypeError` naming the helper. No-argument forms (`|> map`, `|> filter`) are unaffected.
29
+ - Autocomplete no longer suggests `array.toString`, which the evaluator rejects (the method allowlist and the autocomplete catalog now derive from one shared source, so they cannot drift). Method completions also respect `allowedProperties`, matching the evaluator, so a method that would be rejected at runtime is no longer offered.
30
+ - stdlib correctness: `formatDate` now replaces every occurrence of a token (not just the first, so `"YYYY/YYYY"` works); `min`/`max` validate their arguments are numbers and return `undefined` for no arguments instead of leaking `Infinity`/`-Infinity`; `clamp` rejects non-finite bounds and `min > max`; `sort` orders strings by code point for deterministic, locale-independent results. NaN passthrough in the numeric transforms is now documented.
31
+
32
+ ## [0.4.0] - 2026-06-02
33
+
34
+ ### Added
35
+
36
+ - **Context-aware functions** (`addContextFunction`): register functions that receive the evaluation context as their first parameter (typed `Readonly<TCtx>`, passed by reference), enabling auth/permission/personalization patterns without threading context through expression arguments. Pure and context-aware functions share a single namespace; `isContextFunction(name)` introspects the kind.
37
+ - **Generic context typing** (`bonsai<TCtx>()`): the factory is now generic over context type, with end-to-end type safety through `evaluate`, `evaluateSync`, `compile`, and `addContextFunction`. Backward compatible: defaults to `Record<string, unknown>` when unspecified.
38
+ - New exported types: `ContextFunctionFn`, generic `BonsaiPlugin<TCtx>`, generic `CompiledExpression<TCtx>`, generic `BonsaiInstance<TCtx>`.
39
+ - Internal: pure and context-aware functions share one tagged registry, and the cached `Bindings` snapshot consolidates transforms and that registry into a single object passed to the evaluator.
40
+
41
+ ### Resolves
42
+
43
+ - #33: context access from registered functions.
44
+
45
+ ### Credits
46
+
47
+ - Thanks to @jaenyf for raising #33 and contributing PR #34.
48
+
8
49
  ## [0.3.0] - 2026-03-21
9
50
 
10
51
  ### Added
@@ -58,11 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
58
99
  - **Bare dot identity lambda:** `. > 2` and `. * 10` now work as lambda shorthand for the current item itself, in both method calls and pipe transforms (e.g., `[1,2,3,4].filter(. > 2)`, `[1,2,3] |> map(. * 10)`)
59
100
  - Async-safe evaluation for all higher-order array methods — lambda predicates that return Promises are correctly resolved in both top-level and nested contexts via `evaluate()`
60
101
  - Documentation for array methods, lambda shorthand, and method chaining in README and website docs
61
-
62
- ## [Unreleased]
63
-
64
- ### Added
65
-
66
102
  - Deterministic property-based parser and evaluator invariant tests
67
103
  - Random fuzz coverage for malformed parser input
68
104
  - Adversarial regression tests for deep nesting, oversized arrays, nested blocked keys, and spread misuse
package/README.md CHANGED
@@ -125,6 +125,58 @@ expr.addFunction('lookupTier', async (userId) => {
125
125
  await expr.evaluate('lookupTier(userId) == "pro"', { userId: 'u_123' })
126
126
  ```
127
127
 
128
+ ### Context-aware functions
129
+
130
+ Register functions that read the evaluation context directly. The function
131
+ receives the evaluation context as its first parameter (typed read-only), so
132
+ you can keep expressions terse and let the function pull what it needs:
133
+
134
+ ```ts
135
+ import { bonsai } from 'bonsai-js'
136
+
137
+ interface AppContext {
138
+ currentUserId: string
139
+ perms: readonly string[]
140
+ }
141
+
142
+ const app = bonsai<AppContext>()
143
+
144
+ app.addContextFunction('lookupCurrentUserTier', async (ctx) => {
145
+ const row = await db.users.findById(ctx.currentUserId)
146
+ return row?.tier ?? 'free'
147
+ })
148
+
149
+ app.addContextFunction('hasPermission', (ctx, action) =>
150
+ ctx.perms.includes(String(action)))
151
+
152
+ await app.evaluate(
153
+ 'lookupCurrentUserTier() == "pro" && hasPermission("admin")',
154
+ { currentUserId: 'u_123', perms: ['admin', 'write'] },
155
+ )
156
+ ```
157
+
158
+ The instance is generic over the context type (`bonsai<AppContext>()`), giving
159
+ you end-to-end type safety: `ctx` is typed inside the function, and the call
160
+ site is type-checked against the same shape. If your context type has required
161
+ fields, TypeScript also requires you to pass the `context` argument to
162
+ `evaluate`, `evaluateSync`, and compiled-expression evaluation.
163
+
164
+ The context is passed to your function by reference, not copied or frozen. The
165
+ `Readonly<TCtx>` parameter type signals that you should treat it as read-only:
166
+ TypeScript flags reassigning its top-level fields. Bonsai does not deep-freeze
167
+ it, so nested mutation and writes from untyped JavaScript reach the object you
168
+ passed in. If you need isolation between evaluations, pass a fresh context
169
+ object each time.
170
+
171
+ Pure functions (`addFunction`) and context-aware functions (`addContextFunction`)
172
+ share a single namespace. Registering the same name with either method
173
+ overwrites the prior registration, so re-registering a context-aware name with
174
+ `addFunction` turns it back into a pure function (check `isContextFunction(name)`
175
+ if the kind matters). Functions registered with `addFunction` never receive the
176
+ context; reach for `addContextFunction` when a function needs it. Plugins typed
177
+ against a minimal context requirement can still be applied to instances with a
178
+ wider context shape.
179
+
128
180
  ### Editor validation
129
181
 
130
182
  ```ts
@@ -151,6 +203,8 @@ Inside array methods, `.` refers to the current item:
151
203
 
152
204
  Compound predicates work too: `.age >= 18 && .active`
153
205
 
206
+ A lambda is built from the accessor plus operators, member access, and methods. The shorthand is itself a function value, so it cannot be passed *into* another function call: `map(myFn(.x))` does not mean `map(item => myFn(item.x))` and will throw a `BonsaiTypeError`. To transform each item through a function, chain instead: `items |> map(.x) |> map(myFn)` (or `items.map(.x).map(myFn)`).
207
+
154
208
  ### Pipe transforms (via stdlib)
155
209
 
156
210
  ```ts
@@ -244,13 +298,15 @@ const expr = bonsai(options?: BonsaiOptions)
244
298
  | --- | --- | --- | --- |
245
299
  | `timeout` | `number` | `0` | Evaluation timeout in milliseconds. `0` disables the timeout check. |
246
300
  | `maxDepth` | `number` | `100` | Maximum evaluation depth before throwing `BonsaiSecurityError('MAX_DEPTH', ...)`. |
247
- | `maxArrayLength` | `number` | `100000` | Maximum array literal or expanded spread size. |
248
- | `cacheSize` | `number` | `256` | Per-instance cache size for compiled expressions and parsed AST reuse. |
301
+ | `maxArrayLength` | `number` | `100000` | Maximum array size produced during evaluation, including array literals, expanded spread, and array-returning methods (`split`, `map`, `flat`, `concat`, ...). Exceeding it throws `BonsaiSecurityError('MAX_ARRAY_LENGTH', ...)`. |
302
+ | `maxStringLength` | `number` | `100000` | Maximum string size produced by a string-returning method (`padStart`, `padEnd`, `repeat`, `join`, `concat`, `slice`, ...). Applies to the produced length (e.g. `arr.join(sep)` is bounded by its full output, not just the inputs). Exceeding it throws `BonsaiSecurityError('MAX_STRING_LENGTH', ...)`. |
303
+ | `cacheSize` | `number` | `256` | Per-instance cache size for compiled expressions and parsed AST reuse. `0` disables caching. |
249
304
  | `allowedProperties` | `string[]` | `undefined` | Whitelist of allowed member/method names. Does not apply to root identifiers or object-literal keys. |
250
305
  | `deniedProperties` | `string[]` | `undefined` | Denylist of blocked member/method names. Does not apply to root identifiers or object-literal keys. |
251
306
 
252
307
  Important notes:
253
308
 
309
+ - Options are validated at construction: out-of-range values (a negative `cacheSize`, a non-positive `maxDepth`, a negative size limit, a negative/non-finite `timeout`) throw a `RangeError`/`TypeError` immediately rather than failing silently later.
254
310
  - `allowedProperties` and `deniedProperties` apply to **member access** (`obj.name`) and **method calls** (`str.slice()`), not root identifiers (`name`) or object-literal keys (`{ name: value }`).
255
311
  - If you whitelist `user.name`, you must allow both `user` and `name` as member names.
256
312
  - Numeric array indices (e.g., `items[0]`) bypass allow/deny lists automatically.
@@ -361,20 +417,25 @@ evaluateExpression<number>('x * 2', { x: 21 }) // 42
361
417
  ## Instance Methods
362
418
 
363
419
  ```ts
364
- interface BonsaiInstance {
365
- use(plugin: BonsaiPlugin): this
420
+ type EvaluationContextArgs<TCtx extends object = Record<string, unknown>> =
421
+ {} extends TCtx ? [context?: TCtx] : [context: TCtx]
422
+
423
+ interface BonsaiInstance<TCtx extends object = Record<string, unknown>> {
424
+ use(plugin: BonsaiPlugin<TCtx>): this
366
425
  addTransform(name: string, fn: TransformFn): this
367
426
  addFunction(name: string, fn: FunctionFn): this
427
+ addContextFunction(name: string, fn: ContextFunctionFn<TCtx>): this
368
428
  removeTransform(name: string): boolean
369
429
  removeFunction(name: string): boolean
370
430
  hasTransform(name: string): boolean
371
431
  hasFunction(name: string): boolean
432
+ isContextFunction(name: string): boolean
372
433
  listTransforms(): string[]
373
434
  listFunctions(): string[]
374
435
  clearCache(): void
375
- compile(expression: string): CompiledExpression
376
- evaluate<T = unknown>(expression: string, context?: Record<string, unknown>): Promise<T>
377
- evaluateSync<T = unknown>(expression: string, context?: Record<string, unknown>): T
436
+ compile(expression: string): CompiledExpression<TCtx>
437
+ evaluate<T = unknown>(expression: string, ...args: EvaluationContextArgs<TCtx>): Promise<T>
438
+ evaluateSync<T = unknown>(expression: string, ...args: EvaluationContextArgs<TCtx>): T
378
439
  validate(expression: string): ValidationResult
379
440
  }
380
441
  ```
@@ -382,9 +443,12 @@ interface BonsaiInstance {
382
443
  Method notes:
383
444
 
384
445
  - `use()` runs a plugin immediately and returns the same instance.
385
- - `addTransform()` and `addFunction()` overwrite any existing registration with the same name.
386
- - `listTransforms()` and `listFunctions()` return the currently registered names.
446
+ - `addTransform()`, `addFunction()`, and `addContextFunction()` overwrite any existing registration with the same name. Pure and context-aware functions share a single namespace.
447
+ - `addContextFunction()` registers a function that receives the live evaluation context as its first argument (typed read-only; passed by reference, not copied or frozen). See [Context-aware functions](#context-aware-functions).
448
+ - `isContextFunction()` returns `true` if the named function was registered via `addContextFunction()`.
449
+ - `listTransforms()` and `listFunctions()` return the currently registered names. `listFunctions()` includes both pure and context-aware functions.
387
450
  - `clearCache()` clears the internal AST cache and compiled-expression cache. It does not remove registered transforms/functions.
451
+ - Pass a context type generic to `bonsai<MyContext>()` for end-to-end type safety: `evaluate`, `evaluateSync`, `addContextFunction`, `compile`, and `use` all propagate the type. If `MyContext` has required fields, TypeScript requires a context argument when evaluating.
388
452
 
389
453
  ## Extending the Runtime
390
454
 
@@ -470,6 +534,12 @@ Modules:
470
534
  | `dates` | function `now`; transforms `formatDate`, `diffDays` |
471
535
  | `all` | registers every stdlib module above |
472
536
 
537
+ Notes on stdlib semantics:
538
+
539
+ - **`sort`** orders strings by code point (not locale), so results are deterministic across runtimes and locales. Numbers sort numerically.
540
+ - **`min`/`max`** validate that every argument is a number and return `undefined` for no arguments (rather than `Infinity`/`-Infinity`). **`clamp`** requires finite bounds with `min <= max`.
541
+ - **`NaN` is passed through** by the numeric transforms (`sum`, `avg`, `round`, etc.): a `NaN` input yields a `NaN` result (which serializes to `null` in JSON). Validate inputs upstream if you need to reject non-finite numbers.
542
+
473
543
  ## Error Handling
474
544
 
475
545
  Runtime exports:
@@ -532,7 +602,8 @@ Bonsai is designed to safely evaluate expressions, but it is not a process sandb
532
602
  What Bonsai does:
533
603
 
534
604
  - blocks access to `__proto__`, `constructor`, and `prototype` at every access level, even if explicitly allowed
535
- - enforces `maxDepth`, `maxArrayLength`, and optional `timeout`
605
+ - enforces `maxDepth`, `maxArrayLength`, `maxStringLength`, and optional `timeout`
606
+ - bounds parser recursion so pathologically nested input fails closed with a typed `ExpressionError` instead of overflowing the call stack
536
607
  - lets you allowlist or denylist member/method names via `allowedProperties`/`deniedProperties`
537
608
  - prevents expressions from reaching globals or importing modules
538
609
  - looks up root identifiers via own-property checks only (`Object.hasOwn`), so context prototype chains cannot leak
@@ -544,7 +615,7 @@ What Bonsai does:
544
615
  Important operational caveats:
545
616
 
546
617
  - `allowedProperties` and `deniedProperties` apply to member access (`obj.name`) and method calls (`str.slice()`), not root identifiers (`name`) or object-literal keys (`{ name: value }`)
547
- - `timeout` is cooperative and checked during evaluator traversal; it cannot forcibly interrupt arbitrary synchronous code inside your own custom transforms/functions
618
+ - `timeout` is cooperative and checked at evaluator step boundaries (not inside a single native operation); it cannot forcibly interrupt arbitrary synchronous code inside your own custom transforms/functions, so the size limits (`maxArrayLength`, `maxStringLength`) are the primary guard against single-operation resource exhaustion
548
619
  - async transforms/functions are bounded only at awaited boundaries
549
620
  - custom transforms/functions/plugins are trusted host code
550
621
 
@@ -725,8 +796,8 @@ interface Completion {
725
796
 
726
797
  Bonsai follows SemVer for the documented package entrypoints `bonsai-js`, `bonsai-js/stdlib`, and `bonsai-js/autocomplete`.
727
798
 
728
- - Supported runtimes are Node 20+ and current Bun releases.
729
- - The packed npm artifact is smoke-tested on Node 20 and 22.
799
+ - Supported runtimes are Node 22+ and current Bun releases.
800
+ - The packed npm artifact is smoke-tested on Node 22 and 24.
730
801
  - Internal modules under `src/*` are not public API.
731
802
 
732
803
  See [stability policy](./docs/stability-policy.md) for the compatibility boundary and release rules.
@@ -1,4 +1,4 @@
1
- import { c as InferredTypeName, n as BonsaiInstance } from "../types-BhsZZsLY.mjs";
1
+ import { r as BonsaiInstance, u as InferredTypeName } from "../types-DkXfy5IJ.mjs";
2
2
 
3
3
  //#region src/autocomplete/tokenizer.d.ts
4
4
  /** Known phases where errors can occur during completion. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/autocomplete/tokenizer.ts","../../src/autocomplete/completions.ts","../../src/autocomplete/index.ts"],"mappings":";;;;KAcY,eAAA;AAAA,KAOA,YAAA,IAAgB,KAAA,WAAgB,KAAA,EAAO,eAAA;;;KCd9C,gBAAA;EACC,UAAA;EAAwB,YAAA;AAAA;EACxB,UAAA;EAAoB,YAAA;AAAA;AAAA,UAEhB,cAAA;EACR,KAAA;EACA,IAAA;EACA,MAAA;EDO0C;ECL1C,YAAA;AAAA;AAAA,KAGU,UAAA,GAAa,cAAA,GAAiB,gBAAA;;;UCTzB,mBAAA;EACf,OAAA,GAAU,MAAA;EFGe;;AAO3B;;;;EEHE,cAAA,GAAiB,MAAA,SAAe,gBAAA;EFGiB;;;EECjD,OAAA,GAAU,YAAA;AAAA;AAAA,UAGK,oBAAA;EACf,QAAA,CAAS,UAAA,UAAoB,MAAA,WAAiB,UAAA;EAC9C,UAAA,CAAW,OAAA,EAAS,MAAA;AAAA;AAAA,iBAcN,kBAAA,CACd,QAAA,EAAU,cAAA,EACV,OAAA,GAAS,mBAAA,GACR,oBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/autocomplete/tokenizer.ts","../../src/autocomplete/completions.ts","../../src/autocomplete/index.ts"],"mappings":";;;;KAcY,eAAA;AAAA,KAOA,YAAA,IAAgB,KAAA,WAAgB,KAAA,EAAO,eAAe;;;KCd7D,gBAAA;EACC,UAAA;EAAwB,YAAA;AAAA;EACxB,UAAA;EAAoB,YAAA;AAAA;AAAA,UAEhB,cAAA;EACR,KAAA;EACA,IAAA;EACA,MAAA;EDO0C;ECL1C,YAAA;AAAA;AAAA,KAGU,UAAA,GAAa,cAAA,GAAiB,gBAAgB;;;UCTzC,mBAAA;EACf,OAAA,GAAU,MAAA;EFGe;AAAA;AAO3B;;;;EEHE,cAAA,GAAiB,MAAA,SAAe,gBAAA;EFGiB;;;EECjD,OAAA,GAAU,YAAA;AAAA;AAAA,UAGK,oBAAA;EACf,QAAA,CAAS,UAAA,UAAoB,MAAA,WAAiB,UAAA;EAC9C,UAAA,CAAW,OAAA,EAAS,MAAM;AAAA;AAAA,iBAcZ,kBAAA,CACd,QAAA,EAAU,cAAA,EACV,OAAA,GAAS,mBAAA,GACR,oBAAA"}
@@ -1,5 +1,5 @@
1
- import { i as ExpressionError, n as BonsaiSecurityError, r as BonsaiTypeError, t as BonsaiReferenceError } from "../errors-t5i9AEaf.mjs";
2
- import { i as tokenize, t as BLOCKED_PROPERTIES } from "../execution-context-DqYC3L92.mjs";
1
+ import { i as ExpressionError, n as BonsaiSecurityError, r as BonsaiTypeError, t as BonsaiReferenceError } from "../errors-CCZDd1gK.mjs";
2
+ import { a as methodsForReceiverType, o as tokenize, t as BLOCKED_PROPERTIES } from "../execution-context-b6FBfEdv.mjs";
3
3
  //#region src/autocomplete/tokenizer.ts
4
4
  function tolerantTokenize(expression, cursor, onError) {
5
5
  if (expression.length === 0) return {
@@ -66,7 +66,7 @@ function isCursorInsideString(expression, cursor) {
66
66
  }
67
67
  return inSingle || inDouble || inTemplate && templateDepth === 0;
68
68
  }
69
- const TOKEN_RE = /\?\.|\.\.\.|\|>|\?\?|&&|\|\||[!=<>]=?|[+\-*/%]|\*\*|[.(){}[\],?:]|"(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?|`(?:[^`\\$]|\\.|\$(?!\{))*`?|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?|[a-zA-Z_$][\w$]*/g;
69
+ const TOKEN_RE = /\?\.|\.\.\.|\|>|\?\?|&&|\|\||[!=<>]=?|[+\-*/%]|\*\*|[.(){}[\],?:]|"(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?|`(?:[^`\\$]|\\.|\$(?!\{))*`?|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?|[a-zA-Z_$][\w$]*/gu;
70
70
  function regexScan(expression, cursor) {
71
71
  const tokens = [];
72
72
  const text = expression.slice(0, cursor);
@@ -121,13 +121,13 @@ function classifyToken(value, start, end) {
121
121
  start,
122
122
  end
123
123
  };
124
- if (/^[a-zA-Z_$]/.test(value)) return {
124
+ if (/^[a-zA-Z_$]/u.test(value)) return {
125
125
  type: "Identifier",
126
126
  value,
127
127
  start,
128
128
  end
129
129
  };
130
- if (/^\d/.test(value)) return {
130
+ if (/^\d/u.test(value)) return {
131
131
  type: "Number",
132
132
  value,
133
133
  start,
@@ -279,56 +279,15 @@ function buildLambdaState(tokens) {
279
279
  }
280
280
  //#endregion
281
281
  //#region src/autocomplete/catalog.ts
282
- /** Methods allowed per receiver type. Must stay in sync with isAllowedReceiver in eval-ops.ts. */
282
+ /**
283
+ * Methods offered per receiver type, derived from the shared SAFE_METHODS
284
+ * allowlist (src/safe-methods.ts) so autocomplete and the evaluator can never
285
+ * disagree about what is callable.
286
+ */
283
287
  const METHODS_BY_TYPE = {
284
- string: [
285
- "trim",
286
- "trimStart",
287
- "trimEnd",
288
- "toLowerCase",
289
- "toUpperCase",
290
- "startsWith",
291
- "endsWith",
292
- "includes",
293
- "indexOf",
294
- "lastIndexOf",
295
- "slice",
296
- "substring",
297
- "at",
298
- "replace",
299
- "replaceAll",
300
- "split",
301
- "padStart",
302
- "padEnd",
303
- "charAt",
304
- "charCodeAt",
305
- "repeat",
306
- "concat",
307
- "toString"
308
- ],
309
- array: [
310
- "filter",
311
- "map",
312
- "find",
313
- "findIndex",
314
- "some",
315
- "every",
316
- "flatMap",
317
- "join",
318
- "flat",
319
- "includes",
320
- "indexOf",
321
- "lastIndexOf",
322
- "slice",
323
- "at",
324
- "concat",
325
- "toReversed",
326
- "toSorted",
327
- "toSpliced",
328
- "with",
329
- "toString"
330
- ],
331
- number: ["toFixed", "toString"]
288
+ string: methodsForReceiverType("string"),
289
+ array: methodsForReceiverType("array"),
290
+ number: methodsForReceiverType("number")
332
291
  };
333
292
  /** Return type keyed by "receiverType.method" to disambiguate shared method names. */
334
293
  const RETURN_TYPES = {
@@ -374,7 +333,6 @@ const RETURN_TYPES = {
374
333
  "array.includes": "boolean",
375
334
  "array.at": "unknown",
376
335
  "array.with": "array",
377
- "array.toString": "string",
378
336
  "number.toFixed": "string",
379
337
  "number.toString": "string"
380
338
  };
@@ -560,9 +518,12 @@ function addMethodCompletions(results, env) {
560
518
  if (!type || !isMethodReceiverType(type)) return;
561
519
  const cached = METHOD_COMPLETIONS_BY_TYPE[type];
562
520
  if (!cached) return;
563
- if (env.policy.deniedProperties) {
564
- for (const c of cached) if (!env.policy.deniedProperties.has(c.label)) results.push(c);
565
- } else for (const c of cached) results.push(c);
521
+ const { allowedProperties, deniedProperties } = env.policy;
522
+ for (const c of cached) {
523
+ if (allowedProperties && !allowedProperties.has(c.label)) continue;
524
+ if (deniedProperties && deniedProperties.has(c.label)) continue;
525
+ results.push(c);
526
+ }
566
527
  }
567
528
  function addLambdaPropertyCompletions(results, env) {
568
529
  if (!env.lambda?.elementProperties) return;
@@ -686,7 +647,7 @@ function comparator(a, b) {
686
647
  }
687
648
  //#endregion
688
649
  //#region src/autocomplete/index.ts
689
- const VALID_IDENTIFIER = /^[a-zA-Z_$][\w$]*$/;
650
+ const VALID_IDENTIFIER = /^[a-zA-Z_$][\w$]*$/u;
690
651
  /** Check if an error is an expected Bonsai error (syntax, security, type, or reference). */
691
652
  function isExpectedError(err) {
692
653
  return err instanceof ExpressionError || err instanceof BonsaiSecurityError || err instanceof BonsaiTypeError || err instanceof BonsaiReferenceError;
@@ -962,9 +923,9 @@ function probeAcceptedTransforms(instance, inputType, cache, currentTransforms,
962
923
  return accepted;
963
924
  }
964
925
  function inferPipeInputType(expression, cursor, instance, context, onError) {
965
- const pipeMatch = expression.slice(0, cursor).match(/^(.*)\|>\s*\w*\s*$/s);
926
+ const pipeMatch = expression.slice(0, cursor).match(/^(?<input>.*)\|>\s*\w*\s*$/su);
966
927
  if (!pipeMatch) return void 0;
967
- const exprBefore = pipeMatch[1].trim();
928
+ const exprBefore = (pipeMatch.groups?.input ?? "").trim();
968
929
  if (!exprBefore) return void 0;
969
930
  try {
970
931
  const result = instance.evaluateSync(exprBefore, context);