@noorm/broccolidb 2.0.1
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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/TokenCompressionService.d.ts +59 -0
- package/dist/TokenCompressionService.d.ts.map +1 -0
- package/dist/TokenCompressionService.js +179 -0
- package/dist/TokenCompressionService.js.map +1 -0
- package/dist/broccolidb-aggregation.d.ts +14 -0
- package/dist/broccolidb-aggregation.d.ts.map +1 -0
- package/dist/broccolidb-aggregation.js +158 -0
- package/dist/broccolidb-aggregation.js.map +1 -0
- package/dist/broccolidb-cas.d.ts +56 -0
- package/dist/broccolidb-cas.d.ts.map +1 -0
- package/dist/broccolidb-cas.js +285 -0
- package/dist/broccolidb-cas.js.map +1 -0
- package/dist/broccolidb-kernel.d.ts +63 -0
- package/dist/broccolidb-kernel.d.ts.map +1 -0
- package/dist/broccolidb-kernel.js +287 -0
- package/dist/broccolidb-kernel.js.map +1 -0
- package/dist/broccolidb-mutex.d.ts +37 -0
- package/dist/broccolidb-mutex.d.ts.map +1 -0
- package/dist/broccolidb-mutex.js +121 -0
- package/dist/broccolidb-mutex.js.map +1 -0
- package/dist/broccolidb-natural-query.d.ts +13 -0
- package/dist/broccolidb-natural-query.d.ts.map +1 -0
- package/dist/broccolidb-natural-query.js +188 -0
- package/dist/broccolidb-natural-query.js.map +1 -0
- package/dist/broccolidb-table.d.ts +62 -0
- package/dist/broccolidb-table.d.ts.map +1 -0
- package/dist/broccolidb-table.js +893 -0
- package/dist/broccolidb-table.js.map +1 -0
- package/dist/broccolidb-wal.d.ts +49 -0
- package/dist/broccolidb-wal.d.ts.map +1 -0
- package/dist/broccolidb-wal.js +168 -0
- package/dist/broccolidb-wal.js.map +1 -0
- package/dist/broccolidb.contracts.d.ts +232 -0
- package/dist/broccolidb.contracts.d.ts.map +1 -0
- package/dist/broccolidb.contracts.js +7 -0
- package/dist/broccolidb.contracts.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/docs/API.md +208 -0
- package/docs/ARCHITECTURE.md +160 -0
- package/docs/BRIEF.md +56 -0
- package/docs/CONTRIBUTING.md +82 -0
- package/docs/GLOSSARY.md +23 -0
- package/docs/OPERATIONS.md +176 -0
- package/docs/PHILOSOPHY.md +91 -0
- package/docs/README.md +116 -0
- package/docs/RELEASE_NOTES.md +34 -0
- package/docs/TROUBLESHOOTING.md +145 -0
- package/docs/adr/ADR-001-portable-inmemory-kernel.md +74 -0
- package/docs/adr/README.md +38 -0
- package/docs/adr/TEMPLATE.md +35 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BroccoliDB contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# @noorm/broccolidb
|
|
2
|
+
|
|
3
|
+
Portable, dependency-free in-memory tables with explicit file-backed durability.
|
|
4
|
+
|
|
5
|
+
BroccoliDB is an embeddable TypeScript database kernel for applications that
|
|
6
|
+
want a fast table hot path without SQLite, Kysely, a native addon, or a server
|
|
7
|
+
process. Records live in memory for reads and indexes; the kernel persists
|
|
8
|
+
mutations through a micro-batched, checksum-linked write-ahead log (WAL),
|
|
9
|
+
periodic JSON checkpoints, and an optional content-addressable storage (CAS)
|
|
10
|
+
vault for large blobs.
|
|
11
|
+
|
|
12
|
+
> BroccoliDB is a table-first embedded library, not a SQL engine or a remote
|
|
13
|
+
> database service. Its public contracts are TypeScript interfaces and
|
|
14
|
+
> deterministic file formats.
|
|
15
|
+
|
|
16
|
+
[](https://nodejs.org/)
|
|
17
|
+
[](https://www.typescriptlang.org/)
|
|
18
|
+
[](#portability-and-boundaries)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
|
|
21
|
+
## Table of contents
|
|
22
|
+
|
|
23
|
+
- [At a glance](#at-a-glance)
|
|
24
|
+
- [Quick start](#quick-start)
|
|
25
|
+
- [How durability works](#how-durability-works)
|
|
26
|
+
- [Public surface](#public-surface)
|
|
27
|
+
- [Storage layout](#storage-layout)
|
|
28
|
+
- [Portability and boundaries](#portability-and-boundaries)
|
|
29
|
+
- [Documentation](#documentation)
|
|
30
|
+
- [Development and verification](#development-and-verification)
|
|
31
|
+
- [Compatibility policy](#compatibility-policy)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
## At a glance
|
|
35
|
+
|
|
36
|
+
| Capability | What it provides |
|
|
37
|
+
|---|---|
|
|
38
|
+
| In-memory tables | Typed records, CRUD, bulk writes, TTL expiration, snapshots, and reactive change events |
|
|
39
|
+
| Indexes | Equality, sorted/range, composite, and prefix indexes |
|
|
40
|
+
| Queries | Operator filters, boolean clauses, ordering, pagination, a fluent builder, and deterministic natural-language parsing |
|
|
41
|
+
| Aggregation | `sum`, `avg`, `min`, `max`, `count`, `stddev`, grouping, `having`, and grand totals |
|
|
42
|
+
| Durability | Micro-batched WAL frames, checksum validation, checkpoint rotation, replay, and rollback |
|
|
43
|
+
| Blob storage | SHA-256 addressed files, optional Brotli compression, read verification, quarantine, and garbage collection |
|
|
44
|
+
| Coordination | Re-entrant async mutex and kernel-level transaction boundary |
|
|
45
|
+
| Portability | ESM package, Node built-ins only at runtime, no SQLite or native ABI |
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install @noorm/broccolidb
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { BroccoliDatabaseKernel } from "@noorm/broccolidb"
|
|
55
|
+
|
|
56
|
+
type User = {
|
|
57
|
+
id: string
|
|
58
|
+
name: string
|
|
59
|
+
team: string
|
|
60
|
+
active: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const db = new BroccoliDatabaseKernel({ workspaceRoot: "./state" })
|
|
64
|
+
await db.start()
|
|
65
|
+
|
|
66
|
+
const users = db.getTable<User>("users")
|
|
67
|
+
users.createIndex("team")
|
|
68
|
+
users.createIndex("active")
|
|
69
|
+
|
|
70
|
+
await db.transaction(async () => {
|
|
71
|
+
users.put("user-1", { id: "user-1", name: "Ada", team: "platform", active: true })
|
|
72
|
+
users.put("user-2", { id: "user-2", name: "Grace", team: "platform", active: false })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const activePlatformUsers = users.query({
|
|
76
|
+
where: { team: "platform", active: true },
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
console.log(activePlatformUsers)
|
|
80
|
+
console.log(await db.health())
|
|
81
|
+
|
|
82
|
+
await db.checkpoint("after-initial-users")
|
|
83
|
+
await db.stop()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The package is ESM-only. The `workspaceRoot` directory is created on demand;
|
|
87
|
+
the kernel stores its durable state below `workspaceRoot/.broccolidb/`.
|
|
88
|
+
|
|
89
|
+
## How durability works
|
|
90
|
+
|
|
91
|
+
```mermaid
|
|
92
|
+
flowchart LR
|
|
93
|
+
A[Table mutation] --> B[In-memory table and indexes]
|
|
94
|
+
B --> C[WAL frame]
|
|
95
|
+
C --> D[Micro-batched flush]
|
|
96
|
+
D --> E[.broccolidb/wal.log]
|
|
97
|
+
B --> F[Checkpoint]
|
|
98
|
+
F --> G[checkpoint.db]
|
|
99
|
+
F --> H[checkpoints/id.json]
|
|
100
|
+
E --> I[Startup replay]
|
|
101
|
+
G --> I
|
|
102
|
+
I --> B
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A mutation updates the in-memory table immediately and schedules a WAL frame.
|
|
106
|
+
Call `flush()`, use `transaction()`, call `checkpoint()`, or shut down with
|
|
107
|
+
`stop()` when the application needs the buffered frames written before it
|
|
108
|
+
continues. On the next `start()`, BroccoliDB loads the latest base checkpoint
|
|
109
|
+
and replays the remaining WAL frames.
|
|
110
|
+
|
|
111
|
+
For a stable restore point, use:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const checkpoint = await db.checkpoint("before-import")
|
|
115
|
+
// ... perform work ...
|
|
116
|
+
await db.rollback(checkpoint.checkpointId)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The checkpoint record contains a timestamp, frame index, record counts, and a
|
|
120
|
+
SHA-256 hash of the serialized table snapshot. See the [operations guide](docs/OPERATIONS.md)
|
|
121
|
+
for backup, corruption, and multi-process guidance.
|
|
122
|
+
|
|
123
|
+
## Public surface
|
|
124
|
+
|
|
125
|
+
The package entry point re-exports the contracts and implementations needed by
|
|
126
|
+
an embedding application:
|
|
127
|
+
|
|
128
|
+
| Area | Primary exports | Source |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Kernel | `BroccoliDatabaseKernel`, `broccolidb`, `DatabaseKernelOptions` | `src/broccolidb-kernel.ts` |
|
|
131
|
+
| Tables and contracts | `IDbTable`, `DbQueryOptions`, `DbPutOptions`, index/query/change types | `src/broccolidb.contracts.ts` |
|
|
132
|
+
| Queries | `BroccoliNaturalQueryParser`, `IFluentQueryBuilder` | `src/broccolidb-natural-query.ts` and contracts |
|
|
133
|
+
| Aggregation | `DbAggregateQuery`, `DbAggregateResult`, aggregate metrics | `src/broccolidb-aggregation.ts` and contracts |
|
|
134
|
+
| WAL | `BroccoliWriteAheadLog`, `WalIntegrityError`, `WalFrame` | `src/broccolidb-wal.ts` |
|
|
135
|
+
| CAS | `BroccoliCASStorageService`, `StorageIntegrityError` | `src/broccolidb-cas.ts` |
|
|
136
|
+
| Locking | `ReentrantAsyncMutex`, `DatabaseLockError`, `DeadlockTimeoutError` | `src/broccolidb-mutex.ts` |
|
|
137
|
+
| Prompt compression | `TokenCompressionService`, `tokenCompressionService` | `src/TokenCompressionService.ts` |
|
|
138
|
+
|
|
139
|
+
The detailed signatures and examples live in the [API reference](docs/API.md).
|
|
140
|
+
|
|
141
|
+
## Storage layout
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
<workspaceRoot>/
|
|
145
|
+
└── .broccolidb/
|
|
146
|
+
├── wal.log # append-only JSONL mutation journal
|
|
147
|
+
├── wal.log.old # previous journal after checkpoint rotation
|
|
148
|
+
├── checkpoint.db # latest atomic base snapshot
|
|
149
|
+
├── checkpoints/<id>.json # named checkpoint history
|
|
150
|
+
└── cas/
|
|
151
|
+
├── blobs/<00-ff>/<sha> # content-addressed payloads
|
|
152
|
+
└── corrupt/ # quarantined payloads and manifest.jsonl
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Treat this directory as application state. Back it up only while the kernel is
|
|
156
|
+
stopped or after an explicit `flush()`/`checkpoint()`. Do not edit WAL or
|
|
157
|
+
checkpoint files by hand; a malformed WAL frame raises `WalIntegrityError`.
|
|
158
|
+
|
|
159
|
+
## Portability and boundaries
|
|
160
|
+
|
|
161
|
+
BroccoliDB intentionally has:
|
|
162
|
+
|
|
163
|
+
- zero production dependencies in `package.json`;
|
|
164
|
+
- no SQLite, `better-sqlite3`, Kysely, native modules, or Electron ABI coupling;
|
|
165
|
+
- no network service, daemon, or background process;
|
|
166
|
+
- ordinary JSON, JSONL, SHA-256, Brotli, and filesystem primitives;
|
|
167
|
+
- a Node.js `>=18` engine requirement.
|
|
168
|
+
|
|
169
|
+
BroccoliDB does not provide SQL parsing, migrations, a cross-process lock, a
|
|
170
|
+
replicated log, or provider-specific billing/token accounting. The prompt token
|
|
171
|
+
compressor uses a four-characters-per-token estimate as a budget signal, not as
|
|
172
|
+
an API provider billing measurement.
|
|
173
|
+
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
The documentation follows a layered path: **Concepts → How it works →
|
|
177
|
+
Reference → Operations → Decisions**.
|
|
178
|
+
|
|
179
|
+
- [Documentation map](docs/README.md) — choose a reading path by role.
|
|
180
|
+
- [Brief](docs/BRIEF.md) — problem, solution, guarantees, and fit.
|
|
181
|
+
- [Architecture](docs/ARCHITECTURE.md) — layers, lifecycle, files, and failure model.
|
|
182
|
+
- [API reference](docs/API.md) — public types, methods, and query examples.
|
|
183
|
+
- [Operations guide](docs/OPERATIONS.md) — durability, backup, recovery, health, and GC.
|
|
184
|
+
- [Troubleshooting](docs/TROUBLESHOOTING.md) — symptom → cause → action runbooks.
|
|
185
|
+
- [Glossary](docs/GLOSSARY.md) — canonical vocabulary.
|
|
186
|
+
- [Design philosophy](docs/PHILOSOPHY.md) — principles and rejected alternatives.
|
|
187
|
+
- [Architecture decisions](docs/adr/README.md) — durable decisions and their consequences.
|
|
188
|
+
- [Contributing](docs/CONTRIBUTING.md) — source map, workflow, and contract checklist.
|
|
189
|
+
- [Release notes](docs/RELEASE_NOTES.md) — supported package history.
|
|
190
|
+
|
|
191
|
+
## Development and verification
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install
|
|
195
|
+
npm run build
|
|
196
|
+
npm test
|
|
197
|
+
npm run docs:check
|
|
198
|
+
npm run check
|
|
199
|
+
npm pack --dry-run
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`npm test` builds the package and runs the TypeScript tests under `test/`.
|
|
203
|
+
`npm run docs:check` verifies the documentation map and required relative
|
|
204
|
+
links. `npm run check` is the release-oriented local gate.
|
|
205
|
+
|
|
206
|
+
## Compatibility policy
|
|
207
|
+
|
|
208
|
+
The standalone package is the supported BroccoliDB implementation. The removed
|
|
209
|
+
in-repository SQLite implementation is not a compatibility target. New code
|
|
210
|
+
should import `@noorm/broccolidb` from the package entry point and should not
|
|
211
|
+
recreate a private table/WAL implementation inside an application.
|
|
212
|
+
|
|
213
|
+
Changes to exported types, on-disk formats, WAL replay, checkpoint structure, or
|
|
214
|
+
CAS integrity behavior require an API/operations documentation update and an
|
|
215
|
+
entry in the ADR or release notes.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal message shape accepted by the prompt compressor.
|
|
3
|
+
* The generic keeps provider-specific message metadata intact while the
|
|
4
|
+
* compressor only rewrites textual content.
|
|
5
|
+
*/
|
|
6
|
+
export interface TokenCompressionMessage {
|
|
7
|
+
role?: string;
|
|
8
|
+
content?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenCompressionRequest<T extends TokenCompressionMessage = TokenCompressionMessage> {
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
messages: readonly T[];
|
|
13
|
+
requestedModel?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TokenCompressionResult<T extends TokenCompressionMessage = TokenCompressionMessage> {
|
|
16
|
+
compactedSystemPrompt?: string;
|
|
17
|
+
compactedMessages: T[];
|
|
18
|
+
originalEstimatedTokens: number;
|
|
19
|
+
compactedEstimatedTokens: number;
|
|
20
|
+
tokensSaved: number;
|
|
21
|
+
compressionRatioPct: number;
|
|
22
|
+
promptSha256: string;
|
|
23
|
+
/** The requested model is echoed without routing or model substitution. */
|
|
24
|
+
recommendedModel: string;
|
|
25
|
+
cachedL1: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* BroccoliDB implementation of GALXAI's deterministic prompt compression
|
|
29
|
+
* strategy. It is deliberately lossless at the semantic level: only line
|
|
30
|
+
* endings, repeated whitespace, and empty-line runs are normalized. Structured
|
|
31
|
+
* provider blocks remain structured so tool calls, images, and signatures are
|
|
32
|
+
* never serialized into plain text by the optimization pass.
|
|
33
|
+
*/
|
|
34
|
+
export declare class TokenCompressionService {
|
|
35
|
+
private static instance;
|
|
36
|
+
private static readonly MAX_L1_ENTRIES;
|
|
37
|
+
private readonly l1Cache;
|
|
38
|
+
private constructor();
|
|
39
|
+
static getInstance(): TokenCompressionService;
|
|
40
|
+
/**
|
|
41
|
+
* Estimates tokens with the same fast four-characters-per-token heuristic as
|
|
42
|
+
* the source strategy. This is a budget signal, not provider billing data.
|
|
43
|
+
*/
|
|
44
|
+
static estimateTokens(text: string): number;
|
|
45
|
+
/**
|
|
46
|
+
* Compacts a prompt at the request boundary and reuses exact L1 results by
|
|
47
|
+
* SHA-256 of the unmodified prompt payload.
|
|
48
|
+
*/
|
|
49
|
+
compactPrompt<T extends TokenCompressionMessage>(input: TokenCompressionRequest<T>): TokenCompressionResult<T>;
|
|
50
|
+
/** Clears the process-local cache. Intended for lifecycle cleanup and tests. */
|
|
51
|
+
clear(): void;
|
|
52
|
+
private estimatePromptTokens;
|
|
53
|
+
private serializeContent;
|
|
54
|
+
private normalizeContent;
|
|
55
|
+
private normalizeText;
|
|
56
|
+
private copyMessages;
|
|
57
|
+
}
|
|
58
|
+
export declare const tokenCompressionService: TokenCompressionService;
|
|
59
|
+
//# sourceMappingURL=TokenCompressionService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenCompressionService.d.ts","sourceRoot":"","sources":["../src/TokenCompressionService.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,uBAAuB,GAAG,uBAAuB;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,uBAAuB,GAAG,uBAAuB;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,CAAC,EAAE,CAAC;IACvB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAID;;;;;;GAMG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAsC;IAC7D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAS;IAE/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IAEtE,OAAO;WAEO,WAAW,IAAI,uBAAuB;IAOpD;;;OAGG;WACW,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAKlD;;;OAGG;IACI,aAAa,CAAC,CAAC,SAAS,uBAAuB,EACpD,KAAK,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAChC,sBAAsB,CAAC,CAAC,CAAC;IAwD5B,gFAAgF;IACzE,KAAK,IAAI,IAAI;IAIpB,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;IAoDxB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,YAAY;CAarB;AAED,eAAO,MAAM,uBAAuB,yBAAwC,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// [LAYER: CORE]
|
|
2
|
+
// @classification PURE
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
/**
|
|
5
|
+
* BroccoliDB implementation of GALXAI's deterministic prompt compression
|
|
6
|
+
* strategy. It is deliberately lossless at the semantic level: only line
|
|
7
|
+
* endings, repeated whitespace, and empty-line runs are normalized. Structured
|
|
8
|
+
* provider blocks remain structured so tool calls, images, and signatures are
|
|
9
|
+
* never serialized into plain text by the optimization pass.
|
|
10
|
+
*/
|
|
11
|
+
export class TokenCompressionService {
|
|
12
|
+
static instance;
|
|
13
|
+
static MAX_L1_ENTRIES = 1_000;
|
|
14
|
+
l1Cache = new Map();
|
|
15
|
+
constructor() { }
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!TokenCompressionService.instance) {
|
|
18
|
+
TokenCompressionService.instance = new TokenCompressionService();
|
|
19
|
+
}
|
|
20
|
+
return TokenCompressionService.instance;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Estimates tokens with the same fast four-characters-per-token heuristic as
|
|
24
|
+
* the source strategy. This is a budget signal, not provider billing data.
|
|
25
|
+
*/
|
|
26
|
+
static estimateTokens(text) {
|
|
27
|
+
if (!text)
|
|
28
|
+
return 0;
|
|
29
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compacts a prompt at the request boundary and reuses exact L1 results by
|
|
33
|
+
* SHA-256 of the unmodified prompt payload.
|
|
34
|
+
*/
|
|
35
|
+
compactPrompt(input) {
|
|
36
|
+
const rawPayload = JSON.stringify({
|
|
37
|
+
systemPrompt: input.systemPrompt ?? null,
|
|
38
|
+
messages: input.messages,
|
|
39
|
+
});
|
|
40
|
+
const promptSha256 = createHash('sha256').update(rawPayload ?? '').digest('hex');
|
|
41
|
+
const cached = this.l1Cache.get(promptSha256);
|
|
42
|
+
if (cached) {
|
|
43
|
+
return {
|
|
44
|
+
...cached,
|
|
45
|
+
compactedMessages: this.copyMessages(cached.compactedMessages),
|
|
46
|
+
recommendedModel: input.requestedModel ?? 'unknown',
|
|
47
|
+
cachedL1: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const compactedSystemPrompt = input.systemPrompt === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: this.normalizeText(input.systemPrompt, 'system');
|
|
53
|
+
const compactedMessages = input.messages.map((message) => ({
|
|
54
|
+
...message,
|
|
55
|
+
content: this.normalizeContent(message.content, message.role),
|
|
56
|
+
}));
|
|
57
|
+
const originalEstimatedTokens = this.estimatePromptTokens(input.systemPrompt, input.messages);
|
|
58
|
+
const compactedEstimatedTokens = this.estimatePromptTokens(compactedSystemPrompt, compactedMessages);
|
|
59
|
+
const tokensSaved = Math.max(0, originalEstimatedTokens - compactedEstimatedTokens);
|
|
60
|
+
const compressionRatioPct = originalEstimatedTokens > 0
|
|
61
|
+
? Number(((tokensSaved / originalEstimatedTokens) * 100).toFixed(2))
|
|
62
|
+
: 0;
|
|
63
|
+
const result = {
|
|
64
|
+
compactedSystemPrompt,
|
|
65
|
+
compactedMessages,
|
|
66
|
+
originalEstimatedTokens,
|
|
67
|
+
compactedEstimatedTokens,
|
|
68
|
+
tokensSaved,
|
|
69
|
+
compressionRatioPct,
|
|
70
|
+
promptSha256,
|
|
71
|
+
recommendedModel: input.requestedModel ?? 'unknown',
|
|
72
|
+
cachedL1: false,
|
|
73
|
+
};
|
|
74
|
+
this.l1Cache.set(promptSha256, {
|
|
75
|
+
...result,
|
|
76
|
+
compactedMessages: this.copyMessages(compactedMessages),
|
|
77
|
+
});
|
|
78
|
+
if (this.l1Cache.size > TokenCompressionService.MAX_L1_ENTRIES) {
|
|
79
|
+
const firstKey = this.l1Cache.keys().next().value;
|
|
80
|
+
if (firstKey)
|
|
81
|
+
this.l1Cache.delete(firstKey);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/** Clears the process-local cache. Intended for lifecycle cleanup and tests. */
|
|
86
|
+
clear() {
|
|
87
|
+
this.l1Cache.clear();
|
|
88
|
+
}
|
|
89
|
+
estimatePromptTokens(systemPrompt, messages) {
|
|
90
|
+
let total = TokenCompressionService.estimateTokens(systemPrompt ?? '');
|
|
91
|
+
for (const message of messages) {
|
|
92
|
+
total += TokenCompressionService.estimateTokens(this.serializeContent(message.content));
|
|
93
|
+
}
|
|
94
|
+
return total;
|
|
95
|
+
}
|
|
96
|
+
serializeContent(content) {
|
|
97
|
+
if (typeof content === 'string')
|
|
98
|
+
return content;
|
|
99
|
+
if (content === undefined || content === null)
|
|
100
|
+
return '';
|
|
101
|
+
try {
|
|
102
|
+
return JSON.stringify(content) ?? '';
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return String(content);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
normalizeContent(content, role) {
|
|
109
|
+
if (typeof content === 'string') {
|
|
110
|
+
return this.normalizeText(content, role);
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(content))
|
|
113
|
+
return content;
|
|
114
|
+
let changed = false;
|
|
115
|
+
const normalized = content.map((block) => {
|
|
116
|
+
if (!block || typeof block !== 'object')
|
|
117
|
+
return block;
|
|
118
|
+
const record = block;
|
|
119
|
+
if (record.type === 'text' && typeof record.text === 'string') {
|
|
120
|
+
const text = this.normalizeText(record.text, role);
|
|
121
|
+
if (text !== record.text)
|
|
122
|
+
changed = true;
|
|
123
|
+
return text === record.text ? block : { ...record, text };
|
|
124
|
+
}
|
|
125
|
+
if (record.type === 'thinking' && typeof record.thinking === 'string') {
|
|
126
|
+
// Provider thought signatures cover the reasoning payload. Rewriting a
|
|
127
|
+
// signed block would leave the signature attached to different bytes
|
|
128
|
+
// and can make the next request invalid. Unsigned blocks are safe to
|
|
129
|
+
// normalize because they do not carry that provider contract.
|
|
130
|
+
if (record.signature !== undefined)
|
|
131
|
+
return block;
|
|
132
|
+
const thinking = this.normalizeText(record.thinking, role);
|
|
133
|
+
if (thinking !== record.thinking)
|
|
134
|
+
changed = true;
|
|
135
|
+
return thinking === record.thinking ? block : { ...record, thinking };
|
|
136
|
+
}
|
|
137
|
+
if (record.type === 'tool_result') {
|
|
138
|
+
if (typeof record.content === 'string') {
|
|
139
|
+
const text = this.normalizeText(record.content, role);
|
|
140
|
+
if (text !== record.content)
|
|
141
|
+
changed = true;
|
|
142
|
+
return text === record.content ? block : { ...record, content: text };
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(record.content)) {
|
|
145
|
+
const nested = this.normalizeContent(record.content, role);
|
|
146
|
+
if (nested !== record.content) {
|
|
147
|
+
changed = true;
|
|
148
|
+
return { ...record, content: nested };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return block;
|
|
153
|
+
});
|
|
154
|
+
return changed ? normalized : content;
|
|
155
|
+
}
|
|
156
|
+
normalizeText(text, role) {
|
|
157
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
158
|
+
if (role === 'system') {
|
|
159
|
+
return normalized.replace(/[ \t]+/g, ' ').trim();
|
|
160
|
+
}
|
|
161
|
+
return normalized
|
|
162
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
163
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
164
|
+
.trim();
|
|
165
|
+
}
|
|
166
|
+
copyMessages(messages) {
|
|
167
|
+
return messages.map((message) => {
|
|
168
|
+
const content = message.content;
|
|
169
|
+
if (!Array.isArray(content))
|
|
170
|
+
return { ...message };
|
|
171
|
+
return {
|
|
172
|
+
...message,
|
|
173
|
+
content: content.map((block) => (block && typeof block === 'object' ? { ...block } : block)),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export const tokenCompressionService = TokenCompressionService.getInstance();
|
|
179
|
+
//# sourceMappingURL=TokenCompressionService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenCompressionService.js","sourceRoot":"","sources":["../src/TokenCompressionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GALXAI: BroccoliDB Statistical Aggregation & Group-By Engine (Zenith Tier)
|
|
3
|
+
* Single-pass streaming grouping, statistical accumulators (SUM, AVG, MIN, MAX, COUNT, STDDEV),
|
|
4
|
+
* and HAVING predicate filters over BroccoliDbTable records.
|
|
5
|
+
*/
|
|
6
|
+
import type { DbAggregateQuery, DbAggregateResult } from "./broccolidb.contracts.js";
|
|
7
|
+
export declare class BroccoliAggregateEngine {
|
|
8
|
+
/**
|
|
9
|
+
* Executes an aggregation query across candidate records.
|
|
10
|
+
*/
|
|
11
|
+
static execute<T extends Record<string, unknown>>(tableName: string, records: readonly T[], query: DbAggregateQuery): DbAggregateResult;
|
|
12
|
+
private static finalizeMetric;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=broccolidb-aggregation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broccolidb-aggregation.d.ts","sourceRoot":"","sources":["../src/broccolidb-aggregation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,gBAAgB,EAChB,iBAAiB,EAElB,MAAM,2BAA2B,CAAC;AAkBnC,qBAAa,uBAAuB;IAClC;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,KAAK,EAAE,gBAAgB,GACtB,iBAAiB;IA6IpB,OAAO,CAAC,MAAM,CAAC,cAAc;CA6B9B"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GALXAI: BroccoliDB Statistical Aggregation & Group-By Engine (Zenith Tier)
|
|
3
|
+
* Single-pass streaming grouping, statistical accumulators (SUM, AVG, MIN, MAX, COUNT, STDDEV),
|
|
4
|
+
* and HAVING predicate filters over BroccoliDbTable records.
|
|
5
|
+
*/
|
|
6
|
+
export class BroccoliAggregateEngine {
|
|
7
|
+
/**
|
|
8
|
+
* Executes an aggregation query across candidate records.
|
|
9
|
+
*/
|
|
10
|
+
static execute(tableName, records, query) {
|
|
11
|
+
const startTime = performance.now();
|
|
12
|
+
const groupMap = new Map();
|
|
13
|
+
const grandTotalsAccumulator = {};
|
|
14
|
+
for (const [metricKey, metricDef] of Object.entries(query.metrics)) {
|
|
15
|
+
grandTotalsAccumulator[metricKey] = {
|
|
16
|
+
metric: metricDef.metric,
|
|
17
|
+
field: metricDef.field,
|
|
18
|
+
sum: 0,
|
|
19
|
+
min: Number.POSITIVE_INFINITY,
|
|
20
|
+
max: Number.NEGATIVE_INFINITY,
|
|
21
|
+
values: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
for (const record of records) {
|
|
25
|
+
const groupKeyObj = {};
|
|
26
|
+
let compositeKey = "";
|
|
27
|
+
if (query.groupBy && query.groupBy.length > 0) {
|
|
28
|
+
for (const f of query.groupBy) {
|
|
29
|
+
const v = record[f];
|
|
30
|
+
groupKeyObj[f] = v;
|
|
31
|
+
compositeKey += `${f}:${JSON.stringify(v)}|`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
compositeKey = "__root__";
|
|
36
|
+
}
|
|
37
|
+
let group = groupMap.get(compositeKey);
|
|
38
|
+
if (!group) {
|
|
39
|
+
group = {
|
|
40
|
+
keys: groupKeyObj,
|
|
41
|
+
count: 0,
|
|
42
|
+
metricData: {},
|
|
43
|
+
};
|
|
44
|
+
for (const [metricKey, metricDef] of Object.entries(query.metrics)) {
|
|
45
|
+
group.metricData[metricKey] = {
|
|
46
|
+
metric: metricDef.metric,
|
|
47
|
+
field: metricDef.field,
|
|
48
|
+
sum: 0,
|
|
49
|
+
min: Number.POSITIVE_INFINITY,
|
|
50
|
+
max: Number.NEGATIVE_INFINITY,
|
|
51
|
+
values: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
groupMap.set(compositeKey, group);
|
|
55
|
+
}
|
|
56
|
+
group.count++;
|
|
57
|
+
for (const [metricKey, metricDef] of Object.entries(query.metrics)) {
|
|
58
|
+
const acc = group.metricData[metricKey];
|
|
59
|
+
const grandAcc = grandTotalsAccumulator[metricKey];
|
|
60
|
+
const val = metricDef.field ? Number(record[metricDef.field]) : 1;
|
|
61
|
+
const isValidNum = typeof val === "number" && !Number.isNaN(val);
|
|
62
|
+
if (isValidNum) {
|
|
63
|
+
acc.sum += val;
|
|
64
|
+
grandAcc.sum += val;
|
|
65
|
+
if (val < acc.min)
|
|
66
|
+
acc.min = val;
|
|
67
|
+
if (val > acc.max)
|
|
68
|
+
acc.max = val;
|
|
69
|
+
if (val < grandAcc.min)
|
|
70
|
+
grandAcc.min = val;
|
|
71
|
+
if (val > grandAcc.max)
|
|
72
|
+
grandAcc.max = val;
|
|
73
|
+
if (metricDef.metric === "stddev") {
|
|
74
|
+
acc.values.push(val);
|
|
75
|
+
grandAcc.values.push(val);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let groups = [];
|
|
81
|
+
for (const group of groupMap.values()) {
|
|
82
|
+
const finalizedMetrics = {};
|
|
83
|
+
for (const [metricKey, acc] of Object.entries(group.metricData)) {
|
|
84
|
+
finalizedMetrics[metricKey] = this.finalizeMetric(acc.metric, acc.sum, acc.min, acc.max, acc.values, group.count);
|
|
85
|
+
}
|
|
86
|
+
groups.push({
|
|
87
|
+
keys: group.keys,
|
|
88
|
+
metrics: finalizedMetrics,
|
|
89
|
+
recordCount: group.count,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (query.having) {
|
|
93
|
+
groups = groups.filter((g) => {
|
|
94
|
+
for (const [k, expected] of Object.entries(query.having)) {
|
|
95
|
+
const val = k === "count" || k === "_count" ? g.recordCount : g.metrics[k];
|
|
96
|
+
if (expected !== null && typeof expected === "object") {
|
|
97
|
+
const expObj = expected;
|
|
98
|
+
if (expObj.$gt !== undefined && (val === undefined || val <= expObj.$gt))
|
|
99
|
+
return false;
|
|
100
|
+
if (expObj.$gte !== undefined && (val === undefined || val < expObj.$gte))
|
|
101
|
+
return false;
|
|
102
|
+
if (expObj.$lt !== undefined && (val === undefined || val >= expObj.$lt))
|
|
103
|
+
return false;
|
|
104
|
+
if (expObj.$lte !== undefined && (val === undefined || val > expObj.$lte))
|
|
105
|
+
return false;
|
|
106
|
+
if (expObj.$eq !== undefined && val !== expObj.$eq)
|
|
107
|
+
return false;
|
|
108
|
+
if (expObj.$ne !== undefined && val === expObj.$ne)
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
else if (val !== expected) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (query.limit !== undefined && query.limit > 0) {
|
|
119
|
+
groups = groups.slice(0, query.limit);
|
|
120
|
+
}
|
|
121
|
+
const grandTotals = {};
|
|
122
|
+
for (const [metricKey, acc] of Object.entries(grandTotalsAccumulator)) {
|
|
123
|
+
grandTotals[metricKey] = this.finalizeMetric(acc.metric, acc.sum, acc.min, acc.max, acc.values, records.length);
|
|
124
|
+
}
|
|
125
|
+
const executionTimeMicros = Math.round((performance.now() - startTime) * 1000);
|
|
126
|
+
return {
|
|
127
|
+
table: tableName,
|
|
128
|
+
totalRecordsEvaluated: records.length,
|
|
129
|
+
groups,
|
|
130
|
+
grandTotals,
|
|
131
|
+
executionTimeMicros,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
static finalizeMetric(metric, sum, min, max, values, count) {
|
|
135
|
+
switch (metric) {
|
|
136
|
+
case "count":
|
|
137
|
+
return count;
|
|
138
|
+
case "sum":
|
|
139
|
+
return sum;
|
|
140
|
+
case "avg":
|
|
141
|
+
return count > 0 ? Math.round((sum / count) * 10000) / 10000 : 0;
|
|
142
|
+
case "min":
|
|
143
|
+
return min !== Number.POSITIVE_INFINITY ? min : 0;
|
|
144
|
+
case "max":
|
|
145
|
+
return max !== Number.NEGATIVE_INFINITY ? max : 0;
|
|
146
|
+
case "stddev": {
|
|
147
|
+
if (values.length <= 1)
|
|
148
|
+
return 0;
|
|
149
|
+
const mean = sum / values.length;
|
|
150
|
+
const variance = values.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / values.length;
|
|
151
|
+
return Math.round(Math.sqrt(variance) * 10000) / 10000;
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return sum;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=broccolidb-aggregation.js.map
|