prolly-map 0.4.0

Content-addressed versioned map storage primitives.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
# Language Bindings Performance Architecture

Status: phases 1–3 implemented; phase 4 portable-session foundation implemented

Date: 2026-07-16

Scope: Rust binding facade, Go, Python, Kotlin/JVM, Java, Node/TypeScript,
Ruby, Swift, browser/standalone WASM, and shared consumers of prolly reads

## Summary

The Rust prolly implementation now has packed retained nodes, borrowed point
reads, reusable read sessions, and borrowed range/diff/conflict traversal. The
current foreign-language compatibility facade does not preserve most of those
advantages across the runtime boundary. It converts trees, keys, values, and
pages through owned UniFFI records and target-language objects. This keeps the
API safe and portable, but it adds repeated native calls, allocation, decoding,
and copying to the hottest read operations.

The measured Go binding makes the boundary cost visible:

- Rust through Go wins 25 of 30 bulk-write scenarios;
- native Go wins all 30 point-read and all 30 range-scan scenarios;
- the median binding/native-Go latency ratio is `0.50x` for writes, `5.34x`
  for point reads, and `7.24x` for scans; and
- the highest scenario median peak RSS is 10.22 GiB for the binding versus
  6.84 GiB for native Go.

The write result proves that the Rust engine remains competitive when one
boundary transition is amortized over a large batch. The read result shows
that per-operation transport and ownership dominate fine-grained workloads.
The complete evidence and limitations are recorded in
[`../performance-results/go-binding-vs-native-go-final-2026-07-16/report.md`](../performance-results/go-binding-vs-native-go-final-2026-07-16/report.md).

This design adds an optimized binding transport without removing UniFFI or
changing existing owned APIs:

1. retain a root-bound native read session;
2. expose a versioned, narrow hot-path ABI for point reads, multi-get, scans,
   diffs, and conflicts;
3. use packed page buffers with validated offset tables;
4. distinguish owned compatibility methods from callback-scoped or
   lease-scoped view methods;
5. keep Go, JVM, Python, Node, Swift, Ruby, and WASM adapters idiomatic while
   sharing the same Rust session and packed codecs; and
6. extend the same substrate to diff/merge, secondary indexes, and proximity
   queries.

Correctness is non-negotiable. No binding optimization may change key order,
value bytes, cursor semantics, root CIDs, error timing guarantees, merge
results, or persisted formats. No foreign pointer may be retained beyond the
duration permitted by that runtime.

## Delivery Status (2026-07-16)

This design is now implemented through the shared session, Go point/multi-get,
and packed forward-scan phases. The shipped implementation includes:

- `OwnedReadSession`, `OwnedValueLease`, and `OwnedRangeScanSession` in the Rust
  core, with a retained root, bounded route table, recent-leaf locality, and a
  scan stack that seeks once;
- root-bound `ProllyReadSession` objects for memory, file, SQLite, and host
  stores through UniFFI;
- portable owned session methods for point read, ordered multi-get, forward
  range scan, range diff, and three-way conflict inspection;
- regenerated Python, Kotlin/JVM, Ruby, and Swift bindings, plus a Java
  `AutoCloseable` session facade;
- hot ABI v1 operations for caller-buffer point reads, retained value leases,
  packed multi-get, retained forward scans, and packed entry pages;
- opaque registry IDs for sessions, scans, values, and pages, so arbitrary or
  stale IDs are rejected without dereferencing foreign addresses and release is
  idempotent;
- owner-scoped scan handles, pre-allocation limit validation, a 64 MiB packed
  arena ceiling, panic containment, and callbacks invoked without adapter or
  registry locks held; and
- Go legacy-session reuse, explicit `ReadSession`, owned `Get`/`GetMany`/
  `ScanRange`, callback-scoped `GetView`/`ScanRangeView`, and synchronized close.

The following parts remain planned and must not be represented as complete:

- packed retained reverse/prefix, diff, and conflict sessions;
- batched host-language merge resolvers;
- packed mutation input and write-side RSS reduction;
- production transport counters and handle/lease high-water metrics;
- scoped/direct-buffer view adapters for Python, JVM, Swift, Ruby, and WASM;
- Node/WASM session integration; and
- direct secondary-index and proximity-map use of binding page kinds.

The session and opaque-lease substrate deliberately supports those later page
kinds without changing persisted nodes or existing owned APIs.

### Current measured evidence

Release-mode, process-isolated, one-worker in-memory validation on the Apple M2
Max used identical logical Go workloads and native-Go comparison runs. These
measurements include cgo and adapter work and use product-default encodings.

| Tier | Repetitions | Point-view result | Full-scan-view result | Peak RSS signal |
| --- | ---: | --- | --- | --- |
| 10K + 50K | 3 per scenario | Rust won 12/12; 1.17–1.53x faster | Rust lost 12/12; 1.41–2.24x slower | highest median 0.10 vs 0.07 GiB |
| 1M | 1 per scenario | Rust won 6/6; 1.55–1.87x faster | Rust lost 6/6; 1.21–1.49x slower | highest observed 1.42 vs 0.60 GiB |
| 5M | 3 per scenario | Rust won 6/6; 1.55–2.03x faster | Rust won 5/6; 1.15x faster to 1.004x slower | highest median 5.71 vs 3.54 GiB |
| 10M | 3 per scenario | Rust won 6/6; 1.36–2.14x faster | Rust won 6/6; 1.03–1.16x faster | highest median 8.66 vs 6.98 GiB |

The 1M run remains a smoke result, but 5M and 10M now have three-run medians.
At 10M, only four of six point-read cells meet or exceed the desired 1.5x
advantage, so the universal 1.5–2x goal is not yet met. Full scans reach parity
or a modest advantage at large scale, but remain well below a 1.5x advantage.
Binding peak RSS is higher in every large-tier scenario, most visibly for
random mutation. Machine-wide swap rose from 5.64 GiB before the large run to
7.26 GiB after it, so the exact 10M medians should be repeated on an idle
dedicated host before publication. The detailed validated evidence is in
[`../performance-results/go-binding-vs-native-go-large-optimized-2026-07-16/report.md`](../performance-results/go-binding-vs-native-go-large-optimized-2026-07-16/report.md).
Reproduce with `PROLLY_COMPARE_POINT_API=view`,
`PROLLY_COMPARE_SCAN_API=view`, and `scripts/run_go_binding_comparison.sh`.

## Relationship to Existing Designs

This document refines, rather than replaces:

- [`language-bindings-design.md`]language-bindings-design.md, which remains
  authoritative for parity, packaging, the broad UniFFI facade, and release
  policy;
- [`superpowers/specs/2026-07-15-prolly-zero-copy-read-architecture-design.md`]superpowers/specs/2026-07-15-prolly-zero-copy-read-architecture-design.md,
  which remains authoritative for packed Rust nodes, borrowed core APIs,
  `ReadSession`, cache behavior, diff/merge, indexes, and proximity; and
- [`language-store-adapters-design.md`]language-store-adapters-design.md,
  which remains authoritative for host-language stores and async adapter
  packaging.

The earlier binding rule, "copy byte buffers at the boundary," remains valid
for compatibility methods. It is no longer sufficient as the only transport
for performance-critical reads. This design adds explicit optimized methods
whose lifetime and ownership contracts make fewer copies safe.

## Goals

- Preserve all existing public language APIs and their owned-result behavior.
- Make repeated point reads reuse a decoded tree/root and native read session.
- Reduce the common owned point-read path to one native operation and one
  required result copy.
- Provide a zero-per-entry-allocation range path for runtimes that can consume
  callback-scoped or page-lease views.
- Amortize boundary work for multi-get, diff, conflict, and merge-resolution
  workloads.
- Reduce write-side memory amplification without regressing the existing bulk
  write advantage.
- Use one shared Rust implementation for all language packages.
- Make every copy, native transition, retained byte, page, and lease observable.
- Establish repeatable performance gates against direct Rust, compatibility
  bindings, and native-language comparators.

## Non-Goals

- A binding cannot outperform direct Rust for the identical single-key
  operation merely because it is a binding. It always adds a runtime boundary.
- This design does not replace the content-addressed node format or alter CIDs.
- It does not create independent native prolly implementations per language.
- It does not expose unconstrained Rust references to foreign runtimes.
- It does not make asynchronous callbacks borrow Rust memory across an await.
- It does not promise a 1.5x or 2x win before a complete measured result proves
  it for a named workload and machine.
- It does not optimize database or network storage latency by hiding blocking
  I/O behind a synchronous FFI method.

## Findings and Current Copy Ledger

### Direct Rust point read

The optimized native path is:

```text
ReadSession -> retained packed root/routes -> packed leaf -> get_with callback
```

The tree is decoded once, the session retains immutable node bytes, and the
value is borrowed for the callback. A warm hit requires no owned value object.

### Current compatibility point read

The current Go `Engine.Get` is representative of the owned compatibility path:

```text
Go Tree bytes
  -> allocate RustBuffer
  -> copy and decode TreeRecord
Go key
  -> encode length + bytes
  -> allocate RustBuffer
  -> copy and decode Vec<u8>
Rust engine.get
  -> allocate Option<Vec<u8>>
  -> serialize UniFFI result
  -> copy whole RustBuffer into Go
  -> decode and copy value into final []byte
  -> free RustBuffer
```

The engine handle is also cloned through FFI for each wrapper method. The
generated-object ownership of that clone must be audited immediately: if it is
an owned UniFFI handle, every clone requires a corresponding free. Regardless
of ownership, a clone transition per key is unnecessary for a synchronized,
live engine or read-session handle.

### Current range scan

Go `ScanRange` first performs a lower-bound call, then repeatedly calls
`RangePage` with a 1,024-entry limit. A 10M full scan therefore makes roughly
9,766 page calls. Each page serializes the tree, cursor, and end bound; builds
owned Rust entry records; serializes a result buffer; copies the whole result
into Go; and then copies every decoded key and value into its own Go slice.

The cursor contains a resume key, not a retained native traversal stack. The
next page must route back into the tree instead of continuing directly from a
pinned leaf and stack.

### Current bulk write

The Go mutation slice, encoded UniFFI buffer, copied RustBuffer, decoded Rust
mutations, and tree-building state can coexist. Boundary cost is amortized over
the full batch, explaining good write latency, but the duplicate
representations contribute to peak RSS.

### What the benchmark does and does not prove

The benchmark measures the public binding path as a whole. It does not isolate
cgo, handle cloning, UniFFI encoding, allocation, tree decoding, or result
copying independently. Those mechanisms are strongly indicated by code-path
inspection and must be measured separately before attributing a percentage to
any one of them.

## Terminology

### Compatibility facade

The existing broad UniFFI, Node-API, JNI/JVM, Swift, Ruby, Python, Go, and WASM
surface. Its records are self-contained and may be retained by the caller.

### Hot-path ABI

A small versioned native interface for point reads, multi-get, scans, diffs,
and conflict pages. It complements the compatibility facade and is allowed to
use explicit handles, packed buffers, and scoped views.

### Owned result

Target-runtime memory that remains valid after the binding method returns.

### View result

Bytes backed by an immutable native lease or WASM linear-memory window. The
view is valid only for the documented callback or lease lifetime.

### Page lease

An explicit owner of one immutable packed result page. Releasing the lease
invalidates all views into that page.

### Read session

A native object bound to one immutable tree, store, and format identity. It
retains the decoded root, session-local routing state, and bounded locality
metadata.

### Packed page

An ABI-only, non-persisted buffer containing a header, validated offset table,
and contiguous key/value arena.

## Normative Correctness and Safety Invariants

1. Existing owned methods remain source- and behavior-compatible.
2. Owned and view methods return identical key/value bytes and ordering.
3. Binding transport formats never participate in node CIDs or persistence.
4. A read session is bound to exactly one immutable tree/config/store identity.
5. A page or value lease retains every native allocation referenced by a view.
6. A released lease can never be read, resumed, or released twice.
7. Rust never retains a pointer into Go, JVM heap, Python-managed memory,
   JavaScript-managed memory, Swift-managed memory, or Ruby-managed memory
   after the native call returns.
8. Borrowed input pointers are read-only and valid only during the call.
9. Async work owns all bytes that survive an await or thread handoff.
10. No engine, cache, session, or metrics lock is held while invoking a foreign
    callback.
11. Engine/session close synchronizes with active operations and is idempotent.
12. Every packed offset, length, count, tag, and total byte size is validated
    before a foreign slice or buffer view is created.
13. Early stop reports the stopping record as visited, matching current APIs.
14. Store errors after partial streaming preserve the documented partial
    delivery semantics; owned collection methods remain all-or-error.
15. Custom resolver results are validated before any merged root is published.
16. Fast-path failure can fall back to the compatibility method only when doing
    so preserves error and side-effect semantics.

## Architecture Overview

```mermaid
flowchart LR
    A["Language-owned compatibility API"] --> B["Handwritten language adapter"]
    V["Scoped view / batch API"] --> B
    B --> U["Broad UniFFI facade"]
    B --> F["Versioned hot-path transport"]
    U --> C["Binding facade core"]
    F --> S["BindingReadSession / ScanSession"]
    C --> R["Rust prolly core"]
    S --> R
    R --> N["Retained packed nodes"]
    S --> P["Packed value/page leases"]
    P --> B
```

The design has three layers:

1. **Rust core:** existing packed nodes, `ReadSession`, borrowed entry/diff/
   conflict views, and immutable stores.
2. **Binding session and transport:** root-bound sessions, scan cursors, packed
   page codecs, lease registry, and a narrow C/native transport.
3. **Language adapter:** idiomatic owned APIs plus optional scoped-view,
   explicit-session, iterator, and batch APIs.

UniFFI remains the parity and compatibility mechanism. The hot path is not a
second tree implementation; it is another transport into the same binding
facade core.

## Public API Tiers

| Tier | Contract | Typical APIs | Retention |
| --- | --- | --- | --- |
| A | Existing owned compatibility | `Engine.Get`, `Range`, `Diff`, `Merge` | caller may retain results |
| B | Explicit session, owned output | `ReadSession.Get`, `GetMany`, owned scan iterator | caller may retain results |
| C | Scoped view | `GetView`, `ScanRangeView`, `DiffView` | callback or lease scope only |
| D | Packed/batched throughput | packed multi-get/page/conflict batches | explicit page lease |

Tier A is never removed. Tier B should become the recommended API for repeated
reads. Tier C is opt-in and must use names that reveal the lifetime contract.
Tier D is primarily an adapter/internal primitive, but advanced language APIs
may expose a page object where that is idiomatic.

## Native Object Model

### `BindingReadSession`

Conceptually:

```rust
pub struct BindingReadSession {
    engine: Arc<BindingEngineInner>,
    tree: Tree,
    reader: BindingStoreReadSession,
    limits: BindingReadLimits,
    metrics: Arc<BindingMetrics>,
}
```

Requirements:

- decode and validate the tree once at construction;
- retain the root and bounded routing/locality state;
- expose point, multi-get, bound, rank/select, prefix, range, diff, proof-read,
  and index/proximity helpers over the same root;
- be immutable from the caller's perspective;
- support either documented concurrent reads or explicit `clone_for_worker`;
- reject use after close; and
- account retained root, route, node, scratch, and lease bytes.

The initial implementation chooses one session per worker. It does not put a
mutex around every core read to simulate concurrency. Language adapters may
create a bounded cache of sessions keyed by `(root CID, format identity)` for
legacy Tier-A methods, but the cache defaults to at most one recent root and
must be included in cache metrics. Explicit sessions avoid cache ambiguity.

### `BindingScanSession`

A scan session owns:

- a `BindingReadSession` reference;
- start/end or prefix bounds;
- direction;
- the native traversal stack and current retained leaf;
- an ended/closed state; and
- bounded page-building scratch.

It does not serialize a resume key and route from the root for every page.
Compatibility cursor tokens remain supported by the old page methods.

### Diff and conflict sessions

- `BindingDiffSession` owns two compatible read sessions and two traversal
  stacks.
- `BindingConflictSession` owns base, left, and right sessions plus merge
  frontier state.
- Equal-subtree CID skipping remains inside Rust.
- Foreign pages contain only emitted logical records, never internal nodes.

### Handle lifecycle

Every native object has exactly one ownership model:

- object handles are created once and freed once;
- method calls borrow a live handle under adapter close synchronization;
- cloning is explicit and paired with a free;
- finalizers/Cleaners are leak backstops, not correctness mechanisms; and
- debug builds expose live-handle counters grouped by object kind.

The first implementation task is an audit of the current generated and
handwritten clone/free behavior for engine, transaction, blob-store, policy,
callback, page, and session handles.

## Versioned Hot-Path ABI

### ABI policy

- Export `prolly_fast_abi_version()` and capability bits.
- Version the ABI independently from the persisted format and language package.
- Use fixed-width integer fields and explicit little-endian packed buffers.
- Never expose a Rust struct layout directly.
- Use opaque `u64` handles with generation checks or opaque pointers managed by
  one registry; do not mix models.
- Return structured status codes; retrieve detailed owned error bytes only on
  failure.
- Validate limits before allocating or traversing.

### Point read primitives

The owned fast path uses caller-provided output storage:

```c
ProllyGetIntoResult prolly_read_session_get_into(
    uint64_t session,
    const uint8_t *key_ptr,
    size_t key_len,
    uint8_t *out_ptr,
    size_t out_capacity
);
```

The result contains `status`, `found`, `written`, and `required_capacity`.
Input is borrowed only for this call. The common small-value path completes in
one native call. If the output is too small, the adapter grows its buffer and
retries; the session may retain a bounded last-hit lease so a retry copies the
same validated value without retraversing the tree.

The view path returns a lease:

```c
ProllyValueLease prolly_read_session_get_lease(
    uint64_t session,
    const uint8_t *key_ptr,
    size_t key_len
);

void prolly_value_lease_release(uint64_t lease);
```

`ProllyValueLease` contains status, found, lease handle, immutable pointer, and
length. The adapter exposes it only inside a callback or an explicit page/value
object with deterministic close.

### Multi-get primitive

Keys use one packed arena and offset table. Results use one packed page:

```text
key_count | key_offsets[count + 1] | key_arena
```

The Rust session sorts only when the API contract permits it, preserves input
order in the output, deduplicates internal traversal work without changing
duplicate results, and returns found/missing status per requested position.

### Packed entry page format

The first ABI-only page format is:

```text
magic            4 bytes  "PRPG"
version          u16      1
kind             u16      entry, diff, conflict, neighbor, or index match
flags            u32      direction, terminal, optional-field flags
record_count     u32
table_bytes      u32
arena_bytes      u64
records          kind-specific fixed-width offset table
arena            concatenated immutable bytes
```

An entry record contains:

```text
key_offset       u32
key_length       u32
value_offset     u32
value_length     u32
```

Offsets are relative to the arena. ABI v1 caps a page at 65,536 records and a
packed result arena at 64 MiB. The configured byte target is soft for the first
record so a single value up to that hard ceiling can make progress; a larger
single record is rejected and must use the large-value/blob API. Once a page
has a record, a record that would cross the requested target starts the next
page. Every decoder checks magic, version, kind, table size, count
multiplication overflow, arena size, individual ranges, and ordering before
producing views.

The default page policy is the first of 4,096 records or 4 MiB of arena bytes.
Those values are tuning defaults, not ABI. Benchmarks must cover 256, 1,024,
4,096, 16,384, and byte-limited pages before changing them.

### Diff record

A packed diff record contains a kind tag, key range, and optional old/new value
ranges. Absent values use a flag, never an ambiguous zero-length sentinel.

### Conflict record

A packed conflict record contains key plus independently flagged base, left,
and right value ranges. Delete/absence semantics remain identical to the Rust
`ConflictRef` contract.

### Page ownership

- Rust allocates and validates one immutable page.
- The page lease owns the page bytes and any retained node handles used for a
  direct arena representation.
- The adapter may build target-runtime views only while the lease is live.
- Owned APIs copy directly from the page arena into final target objects; they
  do not first copy the whole page and then copy each field again.
- View APIs release the page after the callback or explicit page close.
- A scan callback that stops inside a page closes the scan unless it requested
  explicit page/cursor semantics. It never silently resumes after unconsumed
  records.

## Existing API Compatibility

Existing methods retain their signatures and owned semantics. Their internal
implementation changes as follows:

- `Engine.Get` uses a bounded recent-root session and `get_into`, returning an
  owned target-language byte array.
- `GetMany` uses the packed multi-get primitive and copies only final values.
- Go `ScanRange` uses a native scan session and packed pages, but creates owned
  `Entry` key/value slices before invoking the existing visitor because that
  visitor may retain them.
- Eager `Range` collects from the same scan session with existing error
  semantics.
- Existing diff/conflict pages adapt packed records into owned records.
- Existing merge APIs keep identical resolver and publication behavior.

No existing method begins returning a reused scratch slice or a view whose
lifetime is shorter than before.

## New Language-Neutral APIs

The exact spelling is idiomatic per language, but every binding provides the
same capabilities:

```text
engine.read(tree) -> ReadSession
session.get(key) -> owned optional bytes
session.get_view(key, callback) -> found/outcome
session.get_many(keys) -> owned ordered optionals
session.get_many_page(keys) -> page lease/view
session.scan_range(start, end, callback) -> owned compatibility callback
session.scan_range_view(start, end, callback) -> scoped EntryView
session.scan_prefix_view(prefix, callback)
session.scan_reverse_view(start, end, callback)
session.diff_view(other_session, callback)
session.conflicts_view(left_session, right_session, callback)
```

The owned session API is recommended by default. View APIs are advanced,
explicitly named, and documented adjacent to their lifetime restrictions.

## Language-Specific Adaptation

### Go

- Use the versioned C ABI through cgo for hot paths; keep UniFFI for broad
  parity methods.
- Protect handle use versus `Close` with adapter synchronization instead of
  cloning an engine handle per method.
- Pass `[]byte` input pointers only for the duration of a synchronous call and
  call `runtime.KeepAlive` after native use.
- `ReadSession.Get` uses a reusable small output buffer and returns a correctly
  sized owned slice.
- `GetView` and `ScanRangeView` create slices over C-owned lease memory only
  inside the callback; callers must copy to retain.
- Never pass a Go pointer containing Go pointers to Rust and never retain a Go
  pointer after cgo returns.
- Provide explicit `Close`; finalizers report leaks in debug mode but do not
  define lifetime.
- Run `go test -race` for close/read, concurrent-session, early-stop, and
  callback-reentrancy tests.

Illustrative API:

```go
session, err := engine.Read(tree)
if err != nil { return err }
defer session.Close()

value, found, err := session.Get(key) // owned

found, err = session.GetView(key, func(value []byte) {
    consume(value) // valid only here
})

outcome, err := session.ScanRangeView(start, end, func(entry EntryView) bool {
    consume(entry.Key, entry.Value)
    return true
})
```

### Python

- Keep `bytes` results for compatibility.
- Expose sessions and page iterators in the native extension.
- Expose view APIs as callback-scoped `memoryview` objects whose owner capsule
  retains the lease; invalidate access after close where the runtime permits.
- Release the GIL during Rust traversal and page construction; reacquire it
  before creating Python objects or invoking Python callbacks.
- Never call Python while holding a Rust cache/session lock.
- For normal Python iteration, decode a page at a time to amortize extension
  calls even when final objects are owned `bytes`.

### Kotlin and Java

- Share the Rust session and packed page implementation.
- Compatibility APIs return `ByteArray` and Java `byte[]`.
- View APIs use read-only `ByteBuffer`/direct buffers owned by a page object.
- One JNI/UniFFI transition returns a page; never call JNI once per row.
- `AutoCloseable`/Kotlin `use` closes sessions and pages deterministically.
- `Cleaner` is a leak backstop only.
- Do not expose a direct buffer after its page is closed.

### Node and TypeScript

- Use the shared Rust session directly through Node-API, not the C ABI when a
  native Node wrapper can call Rust without another layer.
- Compatibility APIs return copied `Buffer`/`Uint8Array` values.
- View pages use external read-only Buffers with a finalizer that releases the
  page lease; explicit `close`/`dispose` remains preferred.
- Synchronous callbacks may receive views. Async promises must own or lease
  bytes until resolution and must never borrow stack/session scratch.
- Catch JavaScript exceptions, stop traversal, release the page, and propagate
  the original error.

### Swift

- Compatibility APIs return owned `Data`.
- View APIs use `Data(bytesNoCopy:)` or an explicit `EntryPage` wrapper whose
  deallocator releases the native lease.
- Use deterministic `close` where practical and preserve Swift exclusivity
  rules during callbacks.
- Never reuse page memory while a Swift view is live.

### Ruby

- Compatibility methods return binary-encoded owned strings.
- Prioritize sessions and page batching before exposing borrowed strings,
  because Ruby mutation and retention make view semantics easy to misuse.
- A future view API must return frozen binary strings or an explicit page/view
  object backed by a native lease.

### Browser and standalone WASM

- Compatibility methods return copied `Uint8Array` values.
- Packed pages live in WASM linear memory and are addressed by offsets, never
  host pointers.
- A JavaScript view is valid only until page release and only while memory
  growth cannot invalidate the underlying `ArrayBuffer`.
- The wrapper either prevents memory growth while leases are active or detects
  a changed memory buffer and rejects stale views.
- No view survives an `await` unless it is copied or backed by an explicit
  retained page whose memory-growth policy guarantees stability.

## Point-Read Algorithms

### Owned session get

1. Validate the live session and key length.
2. Borrow the foreign key for the call.
3. Traverse through the retained native session using `get_with`.
4. Copy the value directly into caller-provided output storage.
5. Return found/written/required status.
6. On insufficient capacity, retry from a bounded retained last-hit lease or
   retraverse without changing logical behavior.

The implementation must not construct `TreeRecord`, `Vec<u8>` key, owned Rust
value, serialized UniFFI result, whole-page Go copy, and final value copy for a
single successful hit.

### View get

1. Traverse with `get_with`.
2. Retain the packed leaf/node in a value lease.
3. Return immutable pointer/offset and length.
4. Invoke the language callback or construct an explicit value view.
5. Release the lease deterministically.

### Multi-get

1. Validate the key arena once.
2. Preserve original request positions.
3. Group/sort internal probes only when results can be restored exactly.
4. Reuse session routing and leaf locality across probes.
5. Emit one packed ordered result page.
6. Copy or view results according to the selected API tier.

## Range and Prefix Algorithms

1. Construct one native scan session from the retained read session.
2. Seek once to the lower/upper bound.
3. Retain the traversal stack and current leaf.
4. Fill packed pages directly from borrowed `EntryRef` values.
5. Release internal node handles once the page lease safely owns or copies the
   referenced bytes.
6. Continue from traversal state, not by routing from a serialized resume key.
7. Enforce end/prefix bounds before emitting each record.
8. Report visited count and early stop exactly once.

Increasing page size without retaining traversal state or removing per-field
copies is not considered a complete optimization.

## Diff and Merge

### Diff

- Construct `BindingDiffSession(base, other)` from compatible read sessions.
- Skip equal CIDs in Rust.
- Emit packed diff pages in key order.
- Preserve add/remove/modify tags and absent-value distinctions.
- Adapt pages into owned compatibility records only at the outer boundary.

### Conflict inspection

- Retain base/left/right read sessions.
- Emit packed conflict pages with explicit optional-value flags.
- Keep early-stop and error behavior identical to current conflict scans.

### Merge

- Built-in resolvers execute entirely in Rust and cross the boundary only with
  the final tree/result records.
- Host-language resolvers receive bounded conflict batches where supported.
- Resolver decisions return as a packed decision array keyed by conflict
  ordinal; Rust validates count, ordinal, key identity, decision tag, and value
  limits.
- The compatibility one-conflict callback remains available.
- No root is published until all decisions validate and canonical merge
  application succeeds.

Batching resolver callbacks changes transport, not resolver ordering or merge
semantics.

## Writes

### Packed mutation arena

Define an ABI-only mutation format:

```text
magic/version/count
records: kind + key offset/length + value offset/length
arena: key and value bytes
```

Rust borrows the arena during a synchronous batch call. The core gains a
`MutationRef`/packed preprocessing path so it does not immediately recreate a
`Vec<Vec<u8>>` for every key and value. When ownership is required by sorting,
deduplication, or deferred application, copy once into one Rust-owned arena.

For workloads too large for one foreign arena, add a `BindingBatchWriter`:

- accepts bounded packed chunks;
- owns/spools Rust-side mutations;
- preserves global last-write-wins and canonical final-root semantics;
- finalizes one logical batch; and
- releases language-side chunks after ingestion.

Applying chunks as independent tree mutations is not an acceptable substitute
unless it is proven to produce the same canonical root and stats.

## Secondary Index Foundation

Secondary-index APIs build on the same session and page types:

- bind source and index roots to compatible sessions;
- scan composite index-key ranges with a retained cursor;
- emit packed index-match pages;
- batch source-record joins inside Rust;
- avoid returning source rows that will be filtered immediately in the host;
- keep index/source version and schema checks before query delivery; and
- expose owned matches and scoped `IndexMatchView` variants.

Index build, verification, repair, replacement, and historical query paths must
share the packed mutation/read substrate rather than introduce another FFI
record format.

## Proximity Map Foundation

Proximity queries should cross the language boundary at the highest useful
level:

- pass one query vector and filter/options record;
- perform candidate lookup, filtering, approximate search, exact rerank, and
  deterministic ordering in Rust;
- retain vector/candidate bytes under native handles;
- return only packed top-K neighbor records; and
- copy application payloads only for final neighbors requested by the caller.

Returning every candidate vector to the host language for scoring defeats the
shared Rust engine and is not supported by the fast path. WASM SIMD/alignment
and JVM direct-buffer alignment must be validated independently.

## Proofs, GC, Sync, and Maintenance

These APIs usually return self-contained artifacts and are not initially on the
zero-copy critical path. They still benefit from sessions and packed internal
inspection. Proof bundles, manifests, sync plans, and GC plans remain owned at
the foreign boundary because callers persist or transmit them.

## Async and Cancellation

- An async operation owns all input that survives scheduling.
- A page lease may survive an await only through an explicit owned lease
  object; callback-scoped views never do.
- Go context, Java cancellation, Python cancellation, JavaScript AbortSignal,
  and Swift task cancellation are checked between bounded native pages or
  batches.
- Cancellation closes cursors and releases leases before returning.
- Partial streaming delivery remains documented; owned methods remain
  all-or-error.

## Reentrancy, Errors, and Panics

- Foreign callbacks execute with no Rust cache/session registry lock held.
- Reentry into the same mutable scan cursor is rejected deterministically.
- Reentry through another read session is permitted.
- A foreign exception/panic stops delivery, releases current leases, and maps
  back to the original language error where supported.
- Rust panics never unwind across C/JNI/Node/WASM boundaries.
- Fast status codes distinguish invalid handle, closed handle, buffer too
  small, malformed packed input, store error, corruption, callback failure,
  cancellation, and internal failure.
- Detailed error payload allocation occurs only on failure.

## Observability

Add counters/histograms grouped by language, API tier, and operation:

- FFI/native transitions;
- handle clone/create/free/live counts;
- sessions opened, reused, evicted, and closed;
- tree/config bytes decoded per operation;
- input and output bytes copied;
- RustBuffer allocations/frees;
- target-runtime allocations where measurable;
- value/page leases created, peak live, retained bytes, and leaked-at-finalizer;
- scan pages, records/page, bytes/page, cursor seeks, and retained-stack steps;
- multi-get keys, distinct leaves, reordered probes, and batch width;
- owned versus viewed entries/diffs/conflicts;
- resolver callback count and conflicts/callback;
- mutation input, arena, decoded, scratch, and peak retained bytes; and
- fast-path fallback count and reason.

Production counters must avoid per-row synchronization. Use session-local
aggregation and flush at close/page boundaries. Allocation counting may remain
benchmark-only.

## Testing Strategy

### Semantic equivalence

For every optimized method, compare against the owned compatibility method:

- found and missing point reads;
- empty, one-byte, 100-byte, and large values;
- forward/reverse range and prefix bounds;
- empty tree and empty range;
- early stop at first/middle/last record;
- cursor/page boundaries;
- add/remove/modify diff events;
- delete/absence merge conflicts;
- duplicate multi-get keys;
- mutation duplicate and last-write-wins behavior; and
- secondary-index/proximity ordering and tie breaking.

### Lifetime and resource tests

- use after session/page/value close;
- double close/release;
- engine close racing reads;
- page retained while cache evicts;
- callback exception and reentry;
- finalizer leak detection;
- handle create/free balance over millions of calls;
- Go GC during borrowed input calls;
- JVM/Python/Node/Swift GC while leases are live; and
- WASM memory growth with active/stale pages.

### Malformed input and fuzzing

Fuzz every packed decoder for overflow, truncated headers/tables, invalid tags,
out-of-range offsets, overlapping ranges where forbidden, excessive counts,
oversized arenas, invalid UTF-8 only where text is required, and inconsistent
optional flags. Run Rust fuzzing plus language-level corrupt-page fixtures.

### Concurrency tools

- Rust Miri and sanitizers for lease/session internals;
- ThreadSanitizer where supported;
- Go race detector;
- JVM concurrency tests and Cleaner stress;
- Python subinterpreter/GIL tests where supported; and
- Node worker-thread and async cancellation tests.

## Performance Evaluation

### Required comparison layers

Measure each logical workload at four layers:

1. direct Rust borrowed/session API;
2. existing owned compatibility binding;
3. optimized session-owned API; and
4. optimized view/packed/batch API.

Where a true native implementation exists, add it as a fifth comparator. Never
substitute an old run without identifying that it is cross-run evidence.

### Workload matrix

- sizes: 10K, 50K, 1M, 5M, 10M, and a larger dedicated-host tier;
- arrival/update order: append, deterministic random, and clustered;
- phases: fresh and 30% mutation with the existing insert/update contract;
- values: 0, 1, 16, 64, 100, 1K, and large-value references;
- point reads: hits, misses, mixed hit ratio, hot set, random, and clustered;
- multi-get widths: 1, 8, 32, 128, 1,024, and 16,384;
- scans: full, 1 row, 100 rows, 1%, prefix, reverse, and early stop;
- diffs: identical, sparse, clustered, dense, and structurally misaligned;
- merge: no conflict, sparse conflict, dense conflict, built-in resolver, and
  host callback resolver;
- concurrency: one worker first, then 2/4/8 readers on a dedicated host; and
- stores: in-memory first, then warm/cold file and selected host adapters.

Each scenario records latency, throughput, allocations, copied bytes, native
transitions, peak RSS, lease bytes, validation digest, result count, binary
hash, source hash, and machine state.

### Initial performance gates

Correctness gates are absolute. Performance gates are release criteria for the
optimized path and may be revised only with published evidence:

1. `ReadSession.Get` uses one native read operation in the common-size case and
   improves median point latency by at least 3x versus the current compatibility
   Go path at 1M, 5M, and 10M.
2. Owned `ReadSession.Get` performs at most one target-language value allocation
   on a hit; `GetView` performs zero value allocations.
3. `GetMany` with width 128 or greater amortizes boundary cost enough to target
   at least 1.5x native-Go throughput on the measured in-memory large-tree
   workloads. A miss is reported, not waived.
4. `ScanRangeView` performs zero key/value allocations per entry and at most one
   page allocation/lease per page.
5. Large-tree `ScanRangeView` targets no worse than 1.5x native-Go ns/row;
   existing owned scan targets no worse than 2.5x after removing redundant
   copies.
6. Optimized binding writes do not regress more than 10% from the current
   medians and keep all correctness/root checks.
7. The 10M random-mutation binding median peak RSS targets at most 1.25x the
   native-Go comparator or documents the remaining owned representation.
8. Handle create/free balance is zero at test end; no finalizer is required to
   meet the balance.
9. No optimized path regresses direct Rust or changes persisted bytes.

The 1.5x targets are goals to prove, not claims established by this document.

## Delivery Plan

### Phase 0: Measurement and ownership lock

- add isolated empty-call, handle, input-copy, output-copy, tree-decode,
  point-hit/miss, page-decode, and allocation microbenchmarks;
- add cgo/JNI/Node/Python transition and byte-copy counters;
- audit every clone/free path;
- freeze owned/view equivalence fixtures; and
- run the current matrix on an idle dedicated host.

Exit: every suspected cost has a measurable counter or benchmark and handle
ownership is documented.

### Phase 1: Shared session core

- implement `BindingReadSession` and per-store session enum;
- expose session lifecycle through UniFFI for compatibility;
- add bounded recent-root reuse for legacy methods;
- add close/race/resource metrics and tests.

Exit: repeated reads no longer decode/reacquire the same root internally.

### Phase 2: Go point and multi-get fast path

- implement ABI version/capabilities;
- implement `get_into`, value lease, and packed multi-get;
- remove per-call handle cloning from synchronized Go hot paths;
- add Go owned/session/view APIs and benchmarks.

Exit: point-read and batch gates pass or misses are published with profiles.

### Phase 3: Packed scan pages

- implement page codec/validator and lease registry;
- implement retained forward/reverse/prefix scan sessions;
- migrate Go owned `ScanRange` internally;
- add Go `ScanRangeView` and page-size experiments.

Exit: ordering, early-stop, lifetime, allocation, and scan gates pass.

### Phase 4: Diff, conflict, and merge

- add retained diff/conflict sessions and packed record kinds;
- adapt owned APIs;
- add bounded host-resolver batches;
- verify canonical merge roots against compatibility paths.

### Phase 5: Packed writes and RSS

- add packed mutation input and `MutationRef` preprocessing;
- add bounded batch writer for very large foreign inputs;
- measure memory representation high-water marks;
- preserve canonical roots and stats.

### Phase 6: Other language adapters

Recommended order based on impact and implementation reuse:

1. Node/TypeScript native;
2. Kotlin/Java direct-buffer pages;
3. Python sessions/page iteration;
4. Swift leases;
5. WASM offset pages and memory-growth safety;
6. Ruby sessions/page batching, then views if ergonomically safe.

Every language ships owned compatibility first, then session-owned, then view
APIs after its lifetime tests pass.

### Phase 7: Secondary indexes and proximity

- move index scans/source joins to shared sessions/pages;
- move proximity top-K output to packed neighbor pages;
- benchmark index selectivity, source joins, filter ratios, candidate counts,
  vector dimensions, and top-K independently.

## Source Layout

Proposed ownership:

```text
bindings/uniffi/src/
  lib.rs                 compatibility exports and object wiring
  read_session.rs        BindingReadSession and store dispatch
  scan_session.rs        retained entry/diff/conflict cursors
  packed_page.rs         format, builder, validator, leases
  fast_abi.rs            versioned C ABI exports and status mapping
  packed_mutation.rs     mutation arena and batch writer
  metrics.rs             binding transport metrics

bindings/go/
  prolly.go              existing compatibility surface
  session.go             explicit owned session API
  view.go                scoped point/page views
  fast_abi.go            cgo transport and close synchronization
  packed_page.go         validated lazy page decoder

bindings/<language>/
  generated compatibility bindings
  handwritten session/view adapter
  lifetime and performance tests
```

The exact Rust module split may change during implementation, but hot ABI,
packed codec, session ownership, and language adaptation must not remain one
monolithic generated file.

## Alternatives Rejected

### Increase the Go page size only

This reduces page calls but leaves root reseeks, whole-page copies, and per-key/
value allocations. It is a tuning step after the session/page design.

### Call Go once per Rust row

Per-row Rust-to-Go callbacks replace allocation cost with millions of runtime
transitions and complicate panic/reentrancy behavior. Pages amortize transitions.

### Make existing results borrowed by default

Existing callers may retain byte arrays or entries. Shortening their lifetime
silently is unsafe and source-compatible but behavior-breaking.

### Replace UniFFI entirely

UniFFI provides broad feature parity, generated packaging, and safe owned
records. The optimized transport is deliberately narrow and additive.

### Retain foreign input pointers

This violates runtime movement/GC rules and cgo constraints. Inputs are borrowed
only during synchronous calls or copied into Rust ownership.

### Change the persisted node format

The measured problem is at the language boundary. A wire-format change would
add migration and CID risk without removing UniFFI copies.

### Create native prolly ports per language

This fragments correctness, merge behavior, conformance, and maintenance. The
Rust engine remains authoritative.

## Risks and Mitigations

| Risk | Mitigation |
| --- | --- |
| View used after release | explicit view names, deterministic close, generation-checked leases, debug poisoning |
| Hidden session memory | bounded cache, retained-byte metrics, explicit sessions, eviction tests |
| ABI divergence from UniFFI | shared facade-core functions and owned/view differential tests |
| Handle leak/double free | single ownership model, counters, stress tests, idempotent close |
| Page decoder vulnerability | versioned format, checked arithmetic, fuzzing in Rust and target languages |
| Callback deadlock/reentry | no internal locks during callbacks, same-cursor reentry rejection |
| Async borrow escape | owned/leased async requests only; no callback-scoped view across await |
| Generator replacement | handwritten public adapters isolate generated code and hot ABI |
| Performance overclaim | paired runs, raw data, binary hashes, confidence limitations, honest misses |
| Fast path semantic drift | compatibility oracle, fixtures, root/CID equivalence, staged rollout |

## Acceptance Criteria

The design is complete when:

1. Existing language APIs and fixtures remain compatible.
2. A shared root-bound binding read session exists for every compiled store.
3. The Go hot path avoids repeated tree serialization and per-call object
   cloning.
4. Point-read owned and view results match the compatibility oracle for all
   values and errors.
5. Native scan sessions retain traversal state and packed pages pass fuzzed
   validation.
6. Owned scans copy directly into final language objects; view scans allocate
   no key/value objects per row.
7. Diff, conflict, and merge roots match existing implementations.
8. Packed mutations preserve last-write-wins, ordering, stats, and final CIDs.
9. No foreign pointer outlives its permitted scope and no async borrow crosses
   an await.
10. Handle and lease counts balance without finalizers.
11. Go race, Rust Miri/fuzz/sanitizer, JVM, Python, Node, Swift, Ruby, and WASM
    lifetime suites pass for shipped tiers.
12. Secondary-index and proximity adapters reuse the same session/page/lease
    foundation.
13. Performance gates are run in release mode on a documented host and every
    miss is retained in the report.
14. Persisted bytes, CIDs, conformance digests, result counts, scan ordering,
    and merge results remain unchanged.

## Final Decision

Keep the broad owned UniFFI facade as the stable compatibility and feature
parity layer. Add a narrow, versioned performance transport backed by explicit
root-bound read sessions, retained native cursors, packed validated pages, and
scoped leases. Implement Go first because it has the strongest paired evidence,
then reuse the same Rust substrate through idiomatic adapters for Node, JVM,
Python, Swift, WASM, and Ruby.

The optimized owned API should be the normal application default. Scoped view
and packed-page APIs remain explicit advanced options. This preserves safety
and binding compatibility while giving high-throughput services a path to
amortize the runtime boundary and recover the performance already present in
the Rust prolly core.