bonsai-js 0.2.1 → 0.4.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,61 @@ 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.4.0] - 2026-06-02
11
+
12
+ ### Added
13
+
14
+ - **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.
15
+ - **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.
16
+ - New exported types: `ContextFunctionFn`, generic `BonsaiPlugin<TCtx>`, generic `CompiledExpression<TCtx>`, generic `BonsaiInstance<TCtx>`.
17
+ - 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.
18
+
19
+ ### Resolves
20
+
21
+ - #33: context access from registered functions.
22
+
23
+ ### Credits
24
+
25
+ - Thanks to @jaenyf for raising #33 and contributing PR #34.
26
+
27
+ ## [0.3.0] - 2026-03-21
28
+
29
+ ### Added
30
+
31
+ - **Autocomplete API** (`bonsai-js/autocomplete`): cursor-aware expression completions with property, method, transform, function, and keyword suggestions
32
+ - `getPolicy()` method on `BonsaiInstance` for reading security policy
33
+ - `InferredTypeName`, `PolicySnapshot`, `ResolveResult` shared types
34
+ - Tolerant tokenization for incomplete expressions with regex fallback
35
+ - Type inference from sample context objects with array element type detection
36
+ - Lambda-aware dot classification (lambda-start, lambda member-access, top-level member-access)
37
+ - Security policy filtering in completions (respects allowedProperties/deniedProperties in all paths)
38
+ - Optional chaining (`?.`) support in completions
39
+ - Fuzzy matching with camelCase-aware scoring
40
+ - `onError` callback for debugging autocomplete failures
41
+ - `transformTypes` option to skip auto-probing for performance
42
+ - Eval-based type inference for method chains (e.g., `user.name.trim().`)
43
+ - Nested lambda element inference (e.g., `groups.map(.users.filter(.`)
44
+ - Pre-computed method completion cache for fast method suggestions
45
+ - Autocomplete benchmarks (`benchmarks/autocomplete.bench.ts`)
46
+
47
+ ### Changed
48
+
49
+ - **Performance**: core `evaluateSync` 1.4–2.4x faster (LRU cache fast path, pooled ExecutionContext, leaf node fast path)
50
+ - **Performance**: autocomplete method completions 3–4x faster via pre-computed completion objects
51
+ - LRU cache uses `lastKey` tracking to skip reordering on repeated hits
52
+ - `ExecutionContext` reusable via `reset()` — avoids per-call allocation in `evaluateSync`
53
+ - Leaf AST nodes (literals, identifiers) skip depth tracking overhead in both sync and async evaluators
54
+
55
+ ### Security
56
+
57
+ - Transform names validated as safe identifiers before `evaluateSync` interpolation
58
+ - `resolvePropertyChain` enforces `allowedProperties`/`deniedProperties` policy at every chain step
59
+ - `BLOCKED_NAMES` unified with `BLOCKED_PROPERTIES` from execution context (single source of truth)
60
+ - Catch blocks use `isExpectedError()` checking all 4 Bonsai error types — unexpected errors surface via `onError`
61
+ - Top-level `complete()` catches unexpected errors and returns `[]` (never crashes host)
62
+
8
63
  ## [0.2.1] - 2026-03-16
9
64
 
10
65
  ### Added
@@ -22,11 +77,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
77
  - **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)`)
23
78
  - 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()`
24
79
  - Documentation for array methods, lambda shorthand, and method chaining in README and website docs
25
-
26
- ## [Unreleased]
27
-
28
- ### Added
29
-
30
80
  - Deterministic property-based parser and evaluator invariant tests
31
81
  - Random fuzz coverage for malformed parser input
32
82
  - 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
@@ -361,20 +413,25 @@ evaluateExpression<number>('x * 2', { x: 21 }) // 42
361
413
  ## Instance Methods
362
414
 
363
415
  ```ts
364
- interface BonsaiInstance {
365
- use(plugin: BonsaiPlugin): this
416
+ type EvaluationContextArgs<TCtx extends object = Record<string, unknown>> =
417
+ {} extends TCtx ? [context?: TCtx] : [context: TCtx]
418
+
419
+ interface BonsaiInstance<TCtx extends object = Record<string, unknown>> {
420
+ use(plugin: BonsaiPlugin<TCtx>): this
366
421
  addTransform(name: string, fn: TransformFn): this
367
422
  addFunction(name: string, fn: FunctionFn): this
423
+ addContextFunction(name: string, fn: ContextFunctionFn<TCtx>): this
368
424
  removeTransform(name: string): boolean
369
425
  removeFunction(name: string): boolean
370
426
  hasTransform(name: string): boolean
371
427
  hasFunction(name: string): boolean
428
+ isContextFunction(name: string): boolean
372
429
  listTransforms(): string[]
373
430
  listFunctions(): string[]
374
431
  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
432
+ compile(expression: string): CompiledExpression<TCtx>
433
+ evaluate<T = unknown>(expression: string, ...args: EvaluationContextArgs<TCtx>): Promise<T>
434
+ evaluateSync<T = unknown>(expression: string, ...args: EvaluationContextArgs<TCtx>): T
378
435
  validate(expression: string): ValidationResult
379
436
  }
380
437
  ```
@@ -382,9 +439,12 @@ interface BonsaiInstance {
382
439
  Method notes:
383
440
 
384
441
  - `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.
442
+ - `addTransform()`, `addFunction()`, and `addContextFunction()` overwrite any existing registration with the same name. Pure and context-aware functions share a single namespace.
443
+ - `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).
444
+ - `isContextFunction()` returns `true` if the named function was registered via `addContextFunction()`.
445
+ - `listTransforms()` and `listFunctions()` return the currently registered names. `listFunctions()` includes both pure and context-aware functions.
387
446
  - `clearCache()` clears the internal AST cache and compiled-expression cache. It does not remove registered transforms/functions.
447
+ - 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
448
 
389
449
  ## Extending the Runtime
390
450
 
@@ -578,9 +638,152 @@ Bonsai is optimized for repeated evaluation.
578
638
 
579
639
  Benchmark guidance and current numbers live in the website docs and benchmark suite. Treat raw benchmark numbers as directional, not part of the API contract.
580
640
 
641
+ ## Autocomplete
642
+
643
+ Bonsai ships a cursor-aware autocomplete engine at `bonsai-js/autocomplete`. It provides ranked, type-aware completion suggestions for any cursor position in an expression — designed for rule builders, expression editors, and admin tools. Tree-shakeable: if you don't import it, it's not in your bundle.
644
+
645
+ ```ts
646
+ import { bonsai } from 'bonsai-js'
647
+ import { strings, arrays } from 'bonsai-js/stdlib'
648
+ import { createAutocomplete } from 'bonsai-js/autocomplete'
649
+
650
+ const expr = bonsai().use(strings).use(arrays)
651
+
652
+ const ac = createAutocomplete(expr, {
653
+ context: { user: { name: 'Alice', age: 25 }, items: [1, 2, 3] },
654
+ })
655
+
656
+ ac.complete('user.', 5)
657
+ // [{ label: 'name', detail: 'string', kind: 'property' },
658
+ // { label: 'age', detail: 'number', kind: 'property' }, ...]
659
+
660
+ ac.complete('user.name.', 10)
661
+ // [{ label: 'trim', detail: 'string → string', insertText: 'trim()', cursorOffset: 5 },
662
+ // { label: 'toUpperCase', detail: 'string → string' }, ...]
663
+
664
+ ac.complete('items |> ', 9)
665
+ // Only array-compatible transforms — string-only transforms automatically excluded.
666
+
667
+ ac.complete('users.filter(.', 14)
668
+ // [{ label: 'name', detail: 'string' }, { label: 'age', detail: 'number' }]
669
+ ```
670
+
671
+ ### What it provides
672
+
673
+ | Context | What you get |
674
+ |---|---|
675
+ | `user.` | Object properties with value types |
676
+ | `user?.name?.` | Optional chaining — same completions as dot access |
677
+ | `user.name.` | Type-appropriate methods with return types |
678
+ | `user.name.trim().` | Methods inferred through chained calls (eval-based) |
679
+ | `user.name.to` | Fuzzy-filtered methods via static type inference |
680
+ | `items \|> ` | Transforms filtered by inferred input type |
681
+ | `users.filter(.` | Lambda element properties with types |
682
+ | `users.filter(.name.` | Lambda member — methods for the element property type |
683
+ | `groups.map(.users.filter(.` | Nested lambda element inference |
684
+ | `us` | Context variables, functions, keywords |
685
+ | `name.tLC` | Fuzzy matching (camelCase-aware) |
686
+
687
+ ### How to integrate
688
+
689
+ The API returns pure data — no DOM, no framework dependency. Wire it into any UI:
690
+
691
+ ```ts
692
+ // Custom dropdown
693
+ textarea.addEventListener('input', () => {
694
+ ac.setContext(getCurrentContext())
695
+ const completions = ac.complete(textarea.value, textarea.selectionStart)
696
+ showDropdown(completions)
697
+ })
698
+
699
+ // Monaco editor
700
+ const monacoKindMap = {
701
+ variable: monaco.languages.CompletionItemKind.Variable,
702
+ property: monaco.languages.CompletionItemKind.Property,
703
+ method: monaco.languages.CompletionItemKind.Method,
704
+ transform: monaco.languages.CompletionItemKind.Function,
705
+ function: monaco.languages.CompletionItemKind.Function,
706
+ keyword: monaco.languages.CompletionItemKind.Keyword,
707
+ }
708
+
709
+ monaco.languages.registerCompletionItemProvider('bonsai', {
710
+ triggerCharacters: ['.', '|', '('],
711
+ provideCompletionItems(model, position) {
712
+ ac.setContext(getCurrentContext())
713
+ const offset = model.getOffsetAt(position)
714
+ return {
715
+ suggestions: ac.complete(model.getValue(), offset).map(c => ({
716
+ label: c.label,
717
+ kind: monacoKindMap[c.kind],
718
+ insertText: c.insertText ?? c.label,
719
+ detail: c.detail,
720
+ })),
721
+ }
722
+ },
723
+ })
724
+ ```
725
+
726
+ Context can be updated dynamically — call `ac.setContext(newData)` whenever the user's data changes.
727
+
728
+ ### Options
729
+
730
+ ```ts
731
+ createAutocomplete(instance, {
732
+ // Expression evaluation context — the data your users are writing expressions against
733
+ context: { user: { name: 'Alice' }, items: [1, 2, 3] },
734
+
735
+ // Explicit transform type map — skips auto-probing for better performance.
736
+ // Keys are transform names, values are arrays of accepted input types.
737
+ // When omitted, types are discovered automatically by probing each transform.
738
+ transformTypes: {
739
+ upper: ['string'],
740
+ trim: ['string'],
741
+ count: ['array'],
742
+ sort: ['array'],
743
+ },
744
+
745
+ // Error callback for debugging missing or incorrect completions.
746
+ // Only called for unexpected internal errors — not for expected parse/eval failures.
747
+ onError: (error, phase) => console.warn(`[autocomplete] ${phase}:`, error),
748
+ })
749
+ ```
750
+
751
+ ### Security policy
752
+
753
+ Autocomplete respects the same `allowedProperties` and `deniedProperties` policy configured on the Bonsai instance. Completions are filtered to match what the evaluator would actually allow:
754
+
755
+ ```ts
756
+ const expr = bonsai({ allowedProperties: ['name', 'age'] })
757
+ const ac = createAutocomplete(expr, {
758
+ context: { user: { name: 'Alice', secret: 'hidden' } },
759
+ })
760
+
761
+ ac.complete('user.', 5)
762
+ // 'secret' is excluded — only 'name' and 'age' appear
763
+ ```
764
+
765
+ This applies to all contexts: property access, lambda element properties, and nested chain resolution. Methods (`trim`, `filter`, etc.) are always shown for their applicable types — they are controlled by `deniedProperties`, not `allowedProperties`.
766
+
767
+ ### Completion type
768
+
769
+ ```ts
770
+ interface Completion {
771
+ label: string // Display text and default insert text
772
+ kind: 'variable' | 'property' | 'method' | 'transform' | 'function' | 'keyword'
773
+ detail?: string // Type info: 'string', 'string → array', '"Alice"', 'array(3)'
774
+ insertText?: string // Override insert: 'trim()', 'filter(.)', 'min()'
775
+ cursorOffset?: number // Cursor position in insertText (e.g., between parens)
776
+ sortPriority: number // Lower = higher rank
777
+ }
778
+ ```
779
+
780
+ ### Error handling
781
+
782
+ `complete()` never throws — it always returns `Completion[]`. If an unexpected internal error occurs, it returns `[]` and reports the error via the `onError` callback. Expected errors (syntax errors from incomplete expressions, security policy blocks, type mismatches) are silently handled as part of normal autocomplete operation.
783
+
581
784
  ## Stability
582
785
 
583
- Bonsai follows SemVer for the documented package entrypoints `bonsai-js` and `bonsai-js/stdlib`.
786
+ Bonsai follows SemVer for the documented package entrypoints `bonsai-js`, `bonsai-js/stdlib`, and `bonsai-js/autocomplete`.
584
787
 
585
788
  - Supported runtimes are Node 20+ and current Bun releases.
586
789
  - The packed npm artifact is smoke-tested on Node 20 and 22.
@@ -0,0 +1,47 @@
1
+ import { r as BonsaiInstance, u as InferredTypeName } from "../types-C58c6SlH.mjs";
2
+
3
+ //#region src/autocomplete/tokenizer.d.ts
4
+ /** Known phases where errors can occur during completion. */
5
+ type CompletionPhase = 'tokenize' | 'tryEvalPrefix' | 'inferPipeInputType' | `probeTransform:${string}` | 'complete';
6
+ type ErrorHandler = (error: unknown, phase: CompletionPhase) => void;
7
+ //#endregion
8
+ //#region src/autocomplete/completions.d.ts
9
+ type CompletionInsert = {
10
+ insertText?: undefined;
11
+ cursorOffset?: undefined;
12
+ } | {
13
+ insertText: string;
14
+ cursorOffset?: number;
15
+ };
16
+ interface CompletionBase {
17
+ label: string;
18
+ kind: 'variable' | 'property' | 'method' | 'transform' | 'function' | 'keyword';
19
+ detail?: string;
20
+ /** Lower values sort first. Can become negative after fuzzy-match boosting. */
21
+ sortPriority: number;
22
+ }
23
+ type Completion = CompletionBase & CompletionInsert;
24
+ //#endregion
25
+ //#region src/autocomplete/index.d.ts
26
+ interface AutocompleteOptions {
27
+ context?: Record<string, unknown>;
28
+ /** Map of transform name → accepted input types (e.g., { upper: ['string'], filter: ['array'] }).
29
+ * When set, pipe-transform completions are filtered by the inferred input type.
30
+ * When omitted, auto-probing discovers type compatibility by calling each transform
31
+ * with a sample value — this runs once per type and is cached per instance.
32
+ * For performance-sensitive contexts (e.g., large transform registries), pass this
33
+ * explicitly to avoid the cold-start probe cost. */
34
+ transformTypes?: Record<string, InferredTypeName[]>;
35
+ /** Called when an unexpected internal error occurs during completion.
36
+ * Expected errors (e.g., syntax errors, security blocks, type mismatches) are not reported.
37
+ * Useful for debugging missing or incorrect completions. */
38
+ onError?: ErrorHandler;
39
+ }
40
+ interface AutocompleteInstance {
41
+ complete(expression: string, cursor: number): Completion[];
42
+ setContext(context: Record<string, unknown>): void;
43
+ }
44
+ declare function createAutocomplete(instance: BonsaiInstance, options?: AutocompleteOptions): AutocompleteInstance;
45
+ //#endregion
46
+ export { AutocompleteInstance, AutocompleteOptions, type Completion, createAutocomplete };
47
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +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"}