bonsai-js 0.2.0 → 0.3.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,51 @@ 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
+ ## [0.3.0] - 2026-03-21
9
+
10
+ ### Added
11
+
12
+ - **Autocomplete API** (`bonsai-js/autocomplete`): cursor-aware expression completions with property, method, transform, function, and keyword suggestions
13
+ - `getPolicy()` method on `BonsaiInstance` for reading security policy
14
+ - `InferredTypeName`, `PolicySnapshot`, `ResolveResult` shared types
15
+ - Tolerant tokenization for incomplete expressions with regex fallback
16
+ - Type inference from sample context objects with array element type detection
17
+ - Lambda-aware dot classification (lambda-start, lambda member-access, top-level member-access)
18
+ - Security policy filtering in completions (respects allowedProperties/deniedProperties in all paths)
19
+ - Optional chaining (`?.`) support in completions
20
+ - Fuzzy matching with camelCase-aware scoring
21
+ - `onError` callback for debugging autocomplete failures
22
+ - `transformTypes` option to skip auto-probing for performance
23
+ - Eval-based type inference for method chains (e.g., `user.name.trim().`)
24
+ - Nested lambda element inference (e.g., `groups.map(.users.filter(.`)
25
+ - Pre-computed method completion cache for fast method suggestions
26
+ - Autocomplete benchmarks (`benchmarks/autocomplete.bench.ts`)
27
+
28
+ ### Changed
29
+
30
+ - **Performance**: core `evaluateSync` 1.4–2.4x faster (LRU cache fast path, pooled ExecutionContext, leaf node fast path)
31
+ - **Performance**: autocomplete method completions 3–4x faster via pre-computed completion objects
32
+ - LRU cache uses `lastKey` tracking to skip reordering on repeated hits
33
+ - `ExecutionContext` reusable via `reset()` — avoids per-call allocation in `evaluateSync`
34
+ - Leaf AST nodes (literals, identifiers) skip depth tracking overhead in both sync and async evaluators
35
+
36
+ ### Security
37
+
38
+ - Transform names validated as safe identifiers before `evaluateSync` interpolation
39
+ - `resolvePropertyChain` enforces `allowedProperties`/`deniedProperties` policy at every chain step
40
+ - `BLOCKED_NAMES` unified with `BLOCKED_PROPERTIES` from execution context (single source of truth)
41
+ - Catch blocks use `isExpectedError()` checking all 4 Bonsai error types — unexpected errors surface via `onError`
42
+ - Top-level `complete()` catches unexpected errors and returns `[]` (never crashes host)
43
+
44
+ ## [0.2.1] - 2026-03-16
45
+
46
+ ### Added
47
+
48
+ - Native method support for commonly expected string methods: `trim`, `toLowerCase`, `toUpperCase`, `split`, `padStart`, `padEnd`, `concat`, `lastIndexOf`
49
+ - Native method support for additional array methods: `join`, `flat`, `concat`, `lastIndexOf`, `findIndex`, `flatMap`
50
+ - ES2023 non-mutating array methods: `toReversed`, `toSorted`, `toSpliced`, `with`
51
+ - Mutating array methods (`reverse`, `sort`, `push`, `pop`, `splice`) are explicitly blocked to prevent context mutation
52
+
8
53
  ## [0.2.0] - 2026-03-15
9
54
 
10
55
  ### Added
package/README.md CHANGED
@@ -27,6 +27,8 @@ bun add bonsai-js
27
27
  npm install bonsai-js
28
28
  ```
29
29
 
30
+ [npm](https://www.npmjs.com/package/bonsai-js) · [Playground](https://danfry1.github.io/bonsai-js/playground.html) · [Docs](https://danfry1.github.io/bonsai-js/docs.html)
31
+
30
32
  ## When to use it
31
33
 
32
34
  - Evaluate expressions from config, database records, or admin tools.
@@ -183,6 +185,47 @@ expr.evaluateSync('[1, 2, 3].every(. > 0)') // true
183
185
 
184
186
  Both styles support async evaluation via `evaluate()`.
185
187
 
188
+ ### Built-in safe methods
189
+
190
+ These methods work via native `.method()` syntax without any imports. Mutating methods (`reverse`, `sort`, `push`, `pop`, `splice`, etc.) are blocked to prevent context mutation.
191
+
192
+ **String methods:**
193
+
194
+ | Method | Example |
195
+ | --- | --- |
196
+ | `trim`, `trimStart`, `trimEnd` | `" hi ".trim()` → `"hi"` |
197
+ | `toLowerCase`, `toUpperCase` | `"Hello".toLowerCase()` → `"hello"` |
198
+ | `startsWith`, `endsWith` | `"hello".startsWith("hel")` → `true` |
199
+ | `includes`, `indexOf`, `lastIndexOf` | `"hello".includes("ell")` → `true` |
200
+ | `slice`, `substring`, `at` | `"hello".slice(1, 3)` → `"el"` |
201
+ | `replace`, `replaceAll` | `"abc".replace("a", "x")` → `"xbc"` |
202
+ | `split` | `"a,b,c".split(",")` → `["a", "b", "c"]` |
203
+ | `padStart`, `padEnd` | `"5".padStart(3, "0")` → `"005"` |
204
+ | `charAt`, `charCodeAt`, `repeat`, `concat` | `"ab".repeat(2)` → `"abab"` |
205
+
206
+ **Array methods (with lambda support):**
207
+
208
+ | Method | Example |
209
+ | --- | --- |
210
+ | `filter` | `[1,2,3].filter(. > 1)` → `[2, 3]` |
211
+ | `map` | `[1,2,3].map(. * 10)` → `[10, 20, 30]` |
212
+ | `find`, `findIndex` | `[1,2,3].find(. > 1)` → `2` |
213
+ | `some`, `every` | `[1,2,3].some(. > 2)` → `true` |
214
+ | `flatMap` | `[[1],[2,3]].flatMap(.)` |
215
+
216
+ **Array methods (non-callback):**
217
+
218
+ | Method | Example |
219
+ | --- | --- |
220
+ | `join` | `[1,2,3].join(", ")` → `"1, 2, 3"` |
221
+ | `includes`, `indexOf`, `lastIndexOf` | `[1,2,3].includes(2)` → `true` |
222
+ | `slice`, `at`, `concat`, `flat` | `[1,2,3].concat([4])` → `[1, 2, 3, 4]` |
223
+ | `toReversed`, `toSorted`, `toSpliced`, `with` | `[3,1,2].toSorted()` → `[1, 2, 3]` |
224
+
225
+ **Number methods:** `toFixed`, `toString`
226
+
227
+ **Pipe-only transforms** (require stdlib import): `count`, `first`, `last`, `reverse`, `flatten`, `unique`, `sort`, `upper`, `lower`, `trim`, `sum`, `avg`, `clamp`, and more. See [Standard Library](#standard-library) for the full list.
228
+
186
229
  ## API Reference
187
230
 
188
231
  ### `bonsai(options?)`
@@ -535,9 +578,152 @@ Bonsai is optimized for repeated evaluation.
535
578
 
536
579
  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.
537
580
 
581
+ ## Autocomplete
582
+
583
+ 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.
584
+
585
+ ```ts
586
+ import { bonsai } from 'bonsai-js'
587
+ import { strings, arrays } from 'bonsai-js/stdlib'
588
+ import { createAutocomplete } from 'bonsai-js/autocomplete'
589
+
590
+ const expr = bonsai().use(strings).use(arrays)
591
+
592
+ const ac = createAutocomplete(expr, {
593
+ context: { user: { name: 'Alice', age: 25 }, items: [1, 2, 3] },
594
+ })
595
+
596
+ ac.complete('user.', 5)
597
+ // [{ label: 'name', detail: 'string', kind: 'property' },
598
+ // { label: 'age', detail: 'number', kind: 'property' }, ...]
599
+
600
+ ac.complete('user.name.', 10)
601
+ // [{ label: 'trim', detail: 'string → string', insertText: 'trim()', cursorOffset: 5 },
602
+ // { label: 'toUpperCase', detail: 'string → string' }, ...]
603
+
604
+ ac.complete('items |> ', 9)
605
+ // Only array-compatible transforms — string-only transforms automatically excluded.
606
+
607
+ ac.complete('users.filter(.', 14)
608
+ // [{ label: 'name', detail: 'string' }, { label: 'age', detail: 'number' }]
609
+ ```
610
+
611
+ ### What it provides
612
+
613
+ | Context | What you get |
614
+ |---|---|
615
+ | `user.` | Object properties with value types |
616
+ | `user?.name?.` | Optional chaining — same completions as dot access |
617
+ | `user.name.` | Type-appropriate methods with return types |
618
+ | `user.name.trim().` | Methods inferred through chained calls (eval-based) |
619
+ | `user.name.to` | Fuzzy-filtered methods via static type inference |
620
+ | `items \|> ` | Transforms filtered by inferred input type |
621
+ | `users.filter(.` | Lambda element properties with types |
622
+ | `users.filter(.name.` | Lambda member — methods for the element property type |
623
+ | `groups.map(.users.filter(.` | Nested lambda element inference |
624
+ | `us` | Context variables, functions, keywords |
625
+ | `name.tLC` | Fuzzy matching (camelCase-aware) |
626
+
627
+ ### How to integrate
628
+
629
+ The API returns pure data — no DOM, no framework dependency. Wire it into any UI:
630
+
631
+ ```ts
632
+ // Custom dropdown
633
+ textarea.addEventListener('input', () => {
634
+ ac.setContext(getCurrentContext())
635
+ const completions = ac.complete(textarea.value, textarea.selectionStart)
636
+ showDropdown(completions)
637
+ })
638
+
639
+ // Monaco editor
640
+ const monacoKindMap = {
641
+ variable: monaco.languages.CompletionItemKind.Variable,
642
+ property: monaco.languages.CompletionItemKind.Property,
643
+ method: monaco.languages.CompletionItemKind.Method,
644
+ transform: monaco.languages.CompletionItemKind.Function,
645
+ function: monaco.languages.CompletionItemKind.Function,
646
+ keyword: monaco.languages.CompletionItemKind.Keyword,
647
+ }
648
+
649
+ monaco.languages.registerCompletionItemProvider('bonsai', {
650
+ triggerCharacters: ['.', '|', '('],
651
+ provideCompletionItems(model, position) {
652
+ ac.setContext(getCurrentContext())
653
+ const offset = model.getOffsetAt(position)
654
+ return {
655
+ suggestions: ac.complete(model.getValue(), offset).map(c => ({
656
+ label: c.label,
657
+ kind: monacoKindMap[c.kind],
658
+ insertText: c.insertText ?? c.label,
659
+ detail: c.detail,
660
+ })),
661
+ }
662
+ },
663
+ })
664
+ ```
665
+
666
+ Context can be updated dynamically — call `ac.setContext(newData)` whenever the user's data changes.
667
+
668
+ ### Options
669
+
670
+ ```ts
671
+ createAutocomplete(instance, {
672
+ // Expression evaluation context — the data your users are writing expressions against
673
+ context: { user: { name: 'Alice' }, items: [1, 2, 3] },
674
+
675
+ // Explicit transform type map — skips auto-probing for better performance.
676
+ // Keys are transform names, values are arrays of accepted input types.
677
+ // When omitted, types are discovered automatically by probing each transform.
678
+ transformTypes: {
679
+ upper: ['string'],
680
+ trim: ['string'],
681
+ count: ['array'],
682
+ sort: ['array'],
683
+ },
684
+
685
+ // Error callback for debugging missing or incorrect completions.
686
+ // Only called for unexpected internal errors — not for expected parse/eval failures.
687
+ onError: (error, phase) => console.warn(`[autocomplete] ${phase}:`, error),
688
+ })
689
+ ```
690
+
691
+ ### Security policy
692
+
693
+ Autocomplete respects the same `allowedProperties` and `deniedProperties` policy configured on the Bonsai instance. Completions are filtered to match what the evaluator would actually allow:
694
+
695
+ ```ts
696
+ const expr = bonsai({ allowedProperties: ['name', 'age'] })
697
+ const ac = createAutocomplete(expr, {
698
+ context: { user: { name: 'Alice', secret: 'hidden' } },
699
+ })
700
+
701
+ ac.complete('user.', 5)
702
+ // 'secret' is excluded — only 'name' and 'age' appear
703
+ ```
704
+
705
+ 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`.
706
+
707
+ ### Completion type
708
+
709
+ ```ts
710
+ interface Completion {
711
+ label: string // Display text and default insert text
712
+ kind: 'variable' | 'property' | 'method' | 'transform' | 'function' | 'keyword'
713
+ detail?: string // Type info: 'string', 'string → array', '"Alice"', 'array(3)'
714
+ insertText?: string // Override insert: 'trim()', 'filter(.)', 'min()'
715
+ cursorOffset?: number // Cursor position in insertText (e.g., between parens)
716
+ sortPriority: number // Lower = higher rank
717
+ }
718
+ ```
719
+
720
+ ### Error handling
721
+
722
+ `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.
723
+
538
724
  ## Stability
539
725
 
540
- Bonsai follows SemVer for the documented package entrypoints `bonsai-js` and `bonsai-js/stdlib`.
726
+ Bonsai follows SemVer for the documented package entrypoints `bonsai-js`, `bonsai-js/stdlib`, and `bonsai-js/autocomplete`.
541
727
 
542
728
  - Supported runtimes are Node 20+ and current Bun releases.
543
729
  - The packed npm artifact is smoke-tested on Node 20 and 22.
@@ -0,0 +1,47 @@
1
+ import { c as InferredTypeName, n as BonsaiInstance } from "../types-BhsZZsLY.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"}