@z3rno/sdk 0.8.1 → 0.23.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/.gitattributes +2 -0
- package/Cargo.lock +2784 -0
- package/Cargo.toml +23 -0
- package/README.md +52 -56
- package/build.rs +5 -0
- package/index.d.ts +97 -0
- package/index.js +316 -0
- package/npm/darwin-arm64/README.md +3 -0
- package/npm/darwin-arm64/package.json +26 -0
- package/npm/darwin-x64/README.md +3 -0
- package/npm/darwin-x64/package.json +26 -0
- package/npm/linux-arm64-gnu/README.md +3 -0
- package/npm/linux-arm64-gnu/package.json +29 -0
- package/npm/linux-x64-gnu/README.md +3 -0
- package/npm/linux-x64-gnu/package.json +29 -0
- package/npm/win32-x64-msvc/README.md +3 -0
- package/npm/win32-x64-msvc/package.json +26 -0
- package/package.json +34 -50
- package/src/lib.rs +508 -0
- package/test.js +78 -0
- package/LICENSE +0 -201
- package/dist/index.cjs +0 -1310
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2092
- package/dist/index.d.ts +0 -2092
- package/dist/index.js +0 -1272
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@z3rno/sdk-linux-x64-gnu",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/the-ai-project-co/z3rno"
|
|
7
|
+
},
|
|
8
|
+
"os": [
|
|
9
|
+
"linux"
|
|
10
|
+
],
|
|
11
|
+
"cpu": [
|
|
12
|
+
"x64"
|
|
13
|
+
],
|
|
14
|
+
"main": "z3rno-sdk-native.linux-x64-gnu.node",
|
|
15
|
+
"files": [
|
|
16
|
+
"z3rno-sdk-native.linux-x64-gnu.node"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">= 18"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"libc": [
|
|
27
|
+
"glibc"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@z3rno/sdk-win32-x64-msvc",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/the-ai-project-co/z3rno"
|
|
7
|
+
},
|
|
8
|
+
"os": [
|
|
9
|
+
"win32"
|
|
10
|
+
],
|
|
11
|
+
"cpu": [
|
|
12
|
+
"x64"
|
|
13
|
+
],
|
|
14
|
+
"main": "z3rno-sdk-native.win32-x64-msvc.node",
|
|
15
|
+
"files": [
|
|
16
|
+
"z3rno-sdk-native.win32-x64-msvc.node"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">= 18"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,60 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@z3rno/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js",
|
|
13
|
-
"require": "./dist/index.cjs"
|
|
14
|
-
}
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/the-ai-project-co/z3rno"
|
|
15
8
|
},
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"docs": "typedoc"
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"napi": {
|
|
12
|
+
"name": "z3rno-sdk-native",
|
|
13
|
+
"triples": {
|
|
14
|
+
"defaults": true,
|
|
15
|
+
"additional": [
|
|
16
|
+
"aarch64-unknown-linux-gnu",
|
|
17
|
+
"aarch64-apple-darwin"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
28
20
|
},
|
|
29
|
-
"author": "The AI Project Co. <engineering@z3rno.dev>",
|
|
30
|
-
"homepage": "https://z3rno.dev",
|
|
31
21
|
"engines": {
|
|
32
|
-
"node": ">=18
|
|
22
|
+
"node": ">= 18"
|
|
33
23
|
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": true
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "napi build --platform --release",
|
|
30
|
+
"prepublishOnly": "napi prepublish -t npm",
|
|
31
|
+
"test": "node test.js",
|
|
32
|
+
"version": "napi version"
|
|
36
33
|
},
|
|
37
34
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"prettier": "^3.8.0",
|
|
40
|
-
"tsup": "^8.5.0",
|
|
41
|
-
"typedoc": "^0.28.19",
|
|
42
|
-
"typescript": "^5.7.0",
|
|
43
|
-
"vitest": "^3.2.0"
|
|
35
|
+
"@napi-rs/cli": "^2"
|
|
44
36
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"pgvector",
|
|
52
|
-
"deno",
|
|
53
|
-
"bun"
|
|
54
|
-
],
|
|
55
|
-
"license": "Apache-2.0",
|
|
56
|
-
"repository": {
|
|
57
|
-
"type": "git",
|
|
58
|
-
"url": "https://github.com/the-ai-project-co/z3rno-sdk-typescript"
|
|
37
|
+
"optionalDependencies": {
|
|
38
|
+
"@z3rno/sdk-win32-x64-msvc": "0.23.0",
|
|
39
|
+
"@z3rno/sdk-darwin-x64": "0.23.0",
|
|
40
|
+
"@z3rno/sdk-linux-x64-gnu": "0.23.0",
|
|
41
|
+
"@z3rno/sdk-linux-arm64-gnu": "0.23.0",
|
|
42
|
+
"@z3rno/sdk-darwin-arm64": "0.23.0"
|
|
59
43
|
}
|
|
60
|
-
}
|
|
44
|
+
}
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#![deny(clippy::all)]
|
|
2
|
+
|
|
3
|
+
//! The real napi-rs bindings over `z3rno-engine` (0006.1/0006.2/0006.4):
|
|
4
|
+
//! `Client.connect()` -> `store`/`recall`/`forget`, `client.advanced.audit`.
|
|
5
|
+
//! Async-only — every I/O method returns a native `Promise`, run on napi-rs's
|
|
6
|
+
//! managed Tokio runtime (`tokio_rt` feature). `tier()` is the one exception:
|
|
7
|
+
//! no I/O, so it's a plain sync getter.
|
|
8
|
+
//!
|
|
9
|
+
//! Kept deliberately thin: this module maps napi's JS-facing types onto
|
|
10
|
+
//! `z3rno_engine::MemoryEngine`'s calls. All the actual logic (tier/uuid
|
|
11
|
+
//! parsing, error mapping, DTO conversion) is plain Rust functions with no
|
|
12
|
+
//! napi types in their signatures, so it's unit-tested below with ordinary
|
|
13
|
+
//! `#[test]`s — no JS runtime required to run `cargo test`.
|
|
14
|
+
|
|
15
|
+
use std::sync::Arc;
|
|
16
|
+
|
|
17
|
+
use napi::bindgen_prelude::*;
|
|
18
|
+
use napi_derive::napi;
|
|
19
|
+
use serde_json::Value as JsonValue;
|
|
20
|
+
use uuid::Uuid;
|
|
21
|
+
|
|
22
|
+
use z3rno_engine::backend::BackendError;
|
|
23
|
+
use z3rno_engine::{
|
|
24
|
+
AuditEvent, AuditOperation, BackendTier, ForgetProof, Memory, MemoryEngine, Tier,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const DEFAULT_EMBEDDED_PATH: &str = "z3rno.db";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------
|
|
30
|
+
// Plain Rust helpers — no napi types, unit-tested at the bottom of this
|
|
31
|
+
// file with no live Node/JS runtime needed.
|
|
32
|
+
// ---------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
fn parse_tier(s: &str) -> std::result::Result<Tier, String> {
|
|
35
|
+
match s {
|
|
36
|
+
"working" => Ok(Tier::Working),
|
|
37
|
+
"episodic" => Ok(Tier::Episodic),
|
|
38
|
+
"semantic" => Ok(Tier::Semantic),
|
|
39
|
+
"procedural" => Ok(Tier::Procedural),
|
|
40
|
+
other => Err(format!(
|
|
41
|
+
"invalid tier {other:?}: expected one of \"working\", \"episodic\", \"semantic\", \"procedural\""
|
|
42
|
+
)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn tier_to_js_str(tier: Tier) -> &'static str {
|
|
47
|
+
match tier {
|
|
48
|
+
Tier::Working => "working",
|
|
49
|
+
Tier::Episodic => "episodic",
|
|
50
|
+
Tier::Semantic => "semantic",
|
|
51
|
+
Tier::Procedural => "procedural",
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn backend_tier_to_js_str(tier: BackendTier) -> &'static str {
|
|
56
|
+
match tier {
|
|
57
|
+
BackendTier::Embedded => "embedded",
|
|
58
|
+
BackendTier::Postgres => "postgres",
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn audit_operation_to_js_str(op: AuditOperation) -> &'static str {
|
|
63
|
+
match op {
|
|
64
|
+
AuditOperation::Store => "store",
|
|
65
|
+
AuditOperation::Forget => "forget",
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn parse_uuid(field: &str, s: &str) -> std::result::Result<Uuid, String> {
|
|
70
|
+
Uuid::parse_str(s).map_err(|e| format!("invalid {field} {s:?}: {e}"))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// napi-rs only implements `FromNapiValue` for `f64` (JS's one number
|
|
74
|
+
/// type), not `f32` — so embedding/query vectors cross the FFI boundary as
|
|
75
|
+
/// `Vec<f64>` and get narrowed here before reaching the engine, which
|
|
76
|
+
/// stores vectors as `Vec<f32>`.
|
|
77
|
+
fn to_f32_vec(v: Vec<f64>) -> Vec<f32> {
|
|
78
|
+
v.into_iter().map(|x| x as f32).collect()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Maps a `BackendError` to a `napi::Error`, preserving the original message.
|
|
82
|
+
/// Both variants become `Status::GenericFailure` — neither maps cleanly onto
|
|
83
|
+
/// one of napi's more specific statuses (there's no "not found" status, and
|
|
84
|
+
/// `GenericFailure` is what napi-rs itself recommends for domain errors);
|
|
85
|
+
/// the distinction survives in the message text instead.
|
|
86
|
+
fn map_backend_error(err: BackendError) -> Error {
|
|
87
|
+
match err {
|
|
88
|
+
BackendError::NotFound(msg) => {
|
|
89
|
+
Error::new(Status::GenericFailure, format!("not found: {msg}"))
|
|
90
|
+
}
|
|
91
|
+
BackendError::Storage(e) => {
|
|
92
|
+
Error::new(Status::GenericFailure, format!("storage error: {e}"))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn invalid_arg(message: impl Into<String>) -> Error {
|
|
98
|
+
Error::new(Status::InvalidArg, message.into())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn memory_to_dto(m: Memory) -> MemoryDto {
|
|
102
|
+
MemoryDto {
|
|
103
|
+
id: m.id.to_string(),
|
|
104
|
+
tenant_id: m.tenant_id,
|
|
105
|
+
tier: tier_to_js_str(m.tier).to_string(),
|
|
106
|
+
content: m.content,
|
|
107
|
+
metadata: m.metadata,
|
|
108
|
+
created_at: m.created_at.to_rfc3339(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn forget_proof_to_dto(p: ForgetProof) -> ForgetProofDto {
|
|
113
|
+
ForgetProofDto {
|
|
114
|
+
audit_event_id: p.audit_event_id.to_string(),
|
|
115
|
+
hash: p.hash,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn audit_event_to_dto(e: AuditEvent) -> AuditEventDto {
|
|
120
|
+
AuditEventDto {
|
|
121
|
+
id: e.id.to_string(),
|
|
122
|
+
tenant_id: e.tenant_id,
|
|
123
|
+
operation: audit_operation_to_js_str(e.operation).to_string(),
|
|
124
|
+
memory_id: e.memory_id.to_string(),
|
|
125
|
+
at: e.at.to_rfc3339(),
|
|
126
|
+
prev_hash: e.prev_hash,
|
|
127
|
+
hash: e.hash,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn parse_links(links: Vec<LinkInput>) -> std::result::Result<Vec<(Uuid, String)>, String> {
|
|
132
|
+
links
|
|
133
|
+
.into_iter()
|
|
134
|
+
.map(|l| parse_uuid("targetId", &l.target_id).map(|id| (id, l.relationship)))
|
|
135
|
+
.collect()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------
|
|
139
|
+
// JS-facing DTOs.
|
|
140
|
+
// ---------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/// `client.store(...)` input. `embedding` and `links` are optional (a
|
|
143
|
+
/// memory with no embedding simply isn't recallable by similarity search;
|
|
144
|
+
/// no links means no graph edges).
|
|
145
|
+
#[napi(object)]
|
|
146
|
+
pub struct StoreOptions {
|
|
147
|
+
pub tenant_id: String,
|
|
148
|
+
/// One of "working" | "episodic" | "semantic" | "procedural".
|
|
149
|
+
pub tier: String,
|
|
150
|
+
pub content: String,
|
|
151
|
+
pub embedding: Option<Vec<f64>>,
|
|
152
|
+
pub metadata: JsonValue,
|
|
153
|
+
pub links: Option<Vec<LinkInput>>,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// One graph edge from the memory being stored to an existing memory.
|
|
157
|
+
#[napi(object)]
|
|
158
|
+
pub struct LinkInput {
|
|
159
|
+
pub target_id: String,
|
|
160
|
+
pub relationship: String,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// `client.recall(...)` input.
|
|
164
|
+
#[napi(object)]
|
|
165
|
+
pub struct RecallOptions {
|
|
166
|
+
pub tenant_id: String,
|
|
167
|
+
pub query: Vec<f64>,
|
|
168
|
+
pub k: u32,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// `client.forget(...)` input.
|
|
172
|
+
#[napi(object)]
|
|
173
|
+
pub struct ForgetOptions {
|
|
174
|
+
pub tenant_id: String,
|
|
175
|
+
pub id: String,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// `Client.connect(...)` input. `backend` defaults to `"embedded"`; `path`
|
|
179
|
+
/// defaults to `"z3rno.db"` in the current working directory when the
|
|
180
|
+
/// embedded backend is used and `path` is omitted.
|
|
181
|
+
#[napi(object)]
|
|
182
|
+
pub struct ConnectOptions {
|
|
183
|
+
/// One of "embedded" | "postgres". Defaults to "embedded".
|
|
184
|
+
pub backend: Option<String>,
|
|
185
|
+
pub path: Option<String>,
|
|
186
|
+
pub connection_string: Option<String>,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[napi(object)]
|
|
190
|
+
pub struct MemoryDto {
|
|
191
|
+
pub id: String,
|
|
192
|
+
pub tenant_id: String,
|
|
193
|
+
pub tier: String,
|
|
194
|
+
pub content: String,
|
|
195
|
+
pub metadata: JsonValue,
|
|
196
|
+
/// RFC 3339 timestamp.
|
|
197
|
+
pub created_at: String,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[napi(object)]
|
|
201
|
+
pub struct ForgetProofDto {
|
|
202
|
+
pub audit_event_id: String,
|
|
203
|
+
pub hash: String,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[napi(object)]
|
|
207
|
+
pub struct AuditEventDto {
|
|
208
|
+
pub id: String,
|
|
209
|
+
pub tenant_id: String,
|
|
210
|
+
/// "store" | "forget".
|
|
211
|
+
pub operation: String,
|
|
212
|
+
pub memory_id: String,
|
|
213
|
+
/// RFC 3339 timestamp.
|
|
214
|
+
pub at: String,
|
|
215
|
+
pub prev_hash: Option<String>,
|
|
216
|
+
pub hash: String,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------
|
|
220
|
+
// Client.
|
|
221
|
+
// ---------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
/// A connected z3rno client. Construct with `Client.connect(...)`, never
|
|
224
|
+
/// `new Client()` — connecting the embedded backend is sync but connecting
|
|
225
|
+
/// Postgres needs an `await`, so both go through one consistent async
|
|
226
|
+
/// factory rather than a sync constructor plus a separate async path.
|
|
227
|
+
#[napi]
|
|
228
|
+
pub struct Client {
|
|
229
|
+
engine: Arc<MemoryEngine>,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[napi]
|
|
233
|
+
impl Client {
|
|
234
|
+
#[napi(factory)]
|
|
235
|
+
pub async fn connect(options: Option<ConnectOptions>) -> Result<Client> {
|
|
236
|
+
let options = options.unwrap_or(ConnectOptions {
|
|
237
|
+
backend: None,
|
|
238
|
+
path: None,
|
|
239
|
+
connection_string: None,
|
|
240
|
+
});
|
|
241
|
+
let backend = options.backend.as_deref().unwrap_or("embedded");
|
|
242
|
+
let engine = match backend {
|
|
243
|
+
"embedded" => {
|
|
244
|
+
let path = options
|
|
245
|
+
.path
|
|
246
|
+
.unwrap_or_else(|| DEFAULT_EMBEDDED_PATH.to_string());
|
|
247
|
+
MemoryEngine::embedded(path).map_err(map_backend_error)?
|
|
248
|
+
}
|
|
249
|
+
"postgres" => {
|
|
250
|
+
let connection_string = options.connection_string.ok_or_else(|| {
|
|
251
|
+
invalid_arg("connectionString is required when backend is \"postgres\"")
|
|
252
|
+
})?;
|
|
253
|
+
MemoryEngine::postgres(&connection_string)
|
|
254
|
+
.await
|
|
255
|
+
.map_err(map_backend_error)?
|
|
256
|
+
}
|
|
257
|
+
other => {
|
|
258
|
+
return Err(invalid_arg(format!(
|
|
259
|
+
"invalid backend {other:?}: expected \"embedded\" or \"postgres\""
|
|
260
|
+
)))
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
Ok(Client {
|
|
264
|
+
engine: Arc::new(engine),
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Which tenant-isolation guarantee this client's backend provides:
|
|
269
|
+
/// "embedded" or "postgres". No I/O, so this is sync (not a Promise).
|
|
270
|
+
#[napi]
|
|
271
|
+
pub fn tier(&self) -> String {
|
|
272
|
+
backend_tier_to_js_str(self.engine.tier()).to_string()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[napi]
|
|
276
|
+
pub async fn store(&self, options: StoreOptions) -> Result<MemoryDto> {
|
|
277
|
+
let tier = parse_tier(&options.tier).map_err(invalid_arg)?;
|
|
278
|
+
let links = parse_links(options.links.unwrap_or_default()).map_err(invalid_arg)?;
|
|
279
|
+
let memory = self
|
|
280
|
+
.engine
|
|
281
|
+
.store(
|
|
282
|
+
&options.tenant_id,
|
|
283
|
+
tier,
|
|
284
|
+
options.content,
|
|
285
|
+
options.embedding.map(to_f32_vec),
|
|
286
|
+
options.metadata,
|
|
287
|
+
links,
|
|
288
|
+
)
|
|
289
|
+
.await
|
|
290
|
+
.map_err(map_backend_error)?;
|
|
291
|
+
Ok(memory_to_dto(memory))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[napi]
|
|
295
|
+
pub async fn recall(&self, options: RecallOptions) -> Result<Vec<MemoryDto>> {
|
|
296
|
+
let memories = self
|
|
297
|
+
.engine
|
|
298
|
+
.recall(
|
|
299
|
+
&options.tenant_id,
|
|
300
|
+
to_f32_vec(options.query),
|
|
301
|
+
options.k as usize,
|
|
302
|
+
)
|
|
303
|
+
.await
|
|
304
|
+
.map_err(map_backend_error)?;
|
|
305
|
+
Ok(memories.into_iter().map(memory_to_dto).collect())
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[napi]
|
|
309
|
+
pub async fn forget(&self, options: ForgetOptions) -> Result<Option<ForgetProofDto>> {
|
|
310
|
+
let id = parse_uuid("id", &options.id).map_err(invalid_arg)?;
|
|
311
|
+
let proof = self
|
|
312
|
+
.engine
|
|
313
|
+
.forget(&options.tenant_id, id)
|
|
314
|
+
.await
|
|
315
|
+
.map_err(map_backend_error)?;
|
|
316
|
+
Ok(proof.map(forget_proof_to_dto))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Advanced operations namespace — currently just `.audit(tenantId)`,
|
|
320
|
+
/// the one advanced operation the engine has. Mirrors the engine's own
|
|
321
|
+
/// `MemoryEngine::advanced()` two-tier verb surface.
|
|
322
|
+
#[napi(getter)]
|
|
323
|
+
pub fn advanced(&self) -> Advanced {
|
|
324
|
+
Advanced {
|
|
325
|
+
engine: self.engine.clone(),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[napi]
|
|
331
|
+
pub struct Advanced {
|
|
332
|
+
engine: Arc<MemoryEngine>,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[napi]
|
|
336
|
+
impl Advanced {
|
|
337
|
+
/// The tenant's full audit chain, oldest first.
|
|
338
|
+
#[napi]
|
|
339
|
+
pub async fn audit(&self, tenant_id: String) -> Result<Vec<AuditEventDto>> {
|
|
340
|
+
let events = self
|
|
341
|
+
.engine
|
|
342
|
+
.advanced()
|
|
343
|
+
.audit(&tenant_id)
|
|
344
|
+
.await
|
|
345
|
+
.map_err(map_backend_error)?;
|
|
346
|
+
Ok(events.into_iter().map(audit_event_to_dto).collect())
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[cfg(test)]
|
|
351
|
+
mod tests {
|
|
352
|
+
use super::*;
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn parse_tier_accepts_all_four_snake_case_variants() {
|
|
356
|
+
assert_eq!(parse_tier("working"), Ok(Tier::Working));
|
|
357
|
+
assert_eq!(parse_tier("episodic"), Ok(Tier::Episodic));
|
|
358
|
+
assert_eq!(parse_tier("semantic"), Ok(Tier::Semantic));
|
|
359
|
+
assert_eq!(parse_tier("procedural"), Ok(Tier::Procedural));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#[test]
|
|
363
|
+
fn parse_tier_rejects_unknown_value_with_descriptive_message() {
|
|
364
|
+
let err = parse_tier("bogus").unwrap_err();
|
|
365
|
+
assert!(
|
|
366
|
+
err.contains("bogus"),
|
|
367
|
+
"message should name the bad value: {err}"
|
|
368
|
+
);
|
|
369
|
+
assert!(
|
|
370
|
+
err.contains("working"),
|
|
371
|
+
"message should list valid values: {err}"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn tier_round_trips_through_js_str() {
|
|
377
|
+
for tier in [
|
|
378
|
+
Tier::Working,
|
|
379
|
+
Tier::Episodic,
|
|
380
|
+
Tier::Semantic,
|
|
381
|
+
Tier::Procedural,
|
|
382
|
+
] {
|
|
383
|
+
let s = tier_to_js_str(tier);
|
|
384
|
+
assert_eq!(parse_tier(s), Ok(tier));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[test]
|
|
389
|
+
fn backend_tier_to_js_str_matches_expected_strings() {
|
|
390
|
+
assert_eq!(backend_tier_to_js_str(BackendTier::Embedded), "embedded");
|
|
391
|
+
assert_eq!(backend_tier_to_js_str(BackendTier::Postgres), "postgres");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn audit_operation_to_js_str_matches_expected_strings() {
|
|
396
|
+
assert_eq!(audit_operation_to_js_str(AuditOperation::Store), "store");
|
|
397
|
+
assert_eq!(audit_operation_to_js_str(AuditOperation::Forget), "forget");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn to_f32_vec_narrows_each_element() {
|
|
402
|
+
assert_eq!(
|
|
403
|
+
to_f32_vec(vec![0.1, 0.2, 1.0]),
|
|
404
|
+
vec![0.1f32, 0.2f32, 1.0f32]
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn parse_uuid_accepts_valid_uuid() {
|
|
410
|
+
let id = Uuid::new_v4();
|
|
411
|
+
assert_eq!(parse_uuid("id", &id.to_string()), Ok(id));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[test]
|
|
415
|
+
fn parse_uuid_rejects_garbage_with_field_name_in_message() {
|
|
416
|
+
let err = parse_uuid("targetId", "not-a-uuid").unwrap_err();
|
|
417
|
+
assert!(
|
|
418
|
+
err.contains("targetId"),
|
|
419
|
+
"message should name the field: {err}"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[test]
|
|
424
|
+
fn parse_links_parses_target_ids_and_preserves_relationship() {
|
|
425
|
+
let id = Uuid::new_v4();
|
|
426
|
+
let links = vec![LinkInput {
|
|
427
|
+
target_id: id.to_string(),
|
|
428
|
+
relationship: "caused_by".to_string(),
|
|
429
|
+
}];
|
|
430
|
+
let parsed = parse_links(links).unwrap();
|
|
431
|
+
assert_eq!(parsed, vec![(id, "caused_by".to_string())]);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#[test]
|
|
435
|
+
fn parse_links_surfaces_which_target_id_is_invalid() {
|
|
436
|
+
let links = vec![LinkInput {
|
|
437
|
+
target_id: "nope".to_string(),
|
|
438
|
+
relationship: "r".to_string(),
|
|
439
|
+
}];
|
|
440
|
+
let err = parse_links(links).unwrap_err();
|
|
441
|
+
assert!(err.contains("nope"));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
#[test]
|
|
445
|
+
fn map_backend_error_preserves_not_found_message() {
|
|
446
|
+
let err = map_backend_error(BackendError::NotFound("memory abc123".to_string()));
|
|
447
|
+
assert_eq!(err.status, Status::GenericFailure);
|
|
448
|
+
assert!(err.reason.contains("memory abc123"));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[test]
|
|
452
|
+
fn map_backend_error_preserves_storage_error_message() {
|
|
453
|
+
let err = map_backend_error(BackendError::Storage(anyhow::anyhow!("disk full")));
|
|
454
|
+
assert_eq!(err.status, Status::GenericFailure);
|
|
455
|
+
assert!(err.reason.contains("disk full"));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[test]
|
|
459
|
+
fn invalid_arg_uses_invalid_arg_status() {
|
|
460
|
+
let err = invalid_arg("bad input");
|
|
461
|
+
assert_eq!(err.status, Status::InvalidArg);
|
|
462
|
+
assert_eq!(err.reason, "bad input");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#[test]
|
|
466
|
+
fn memory_to_dto_maps_every_field() {
|
|
467
|
+
let id = Uuid::new_v4();
|
|
468
|
+
let memory = Memory {
|
|
469
|
+
id,
|
|
470
|
+
tenant_id: "t1".to_string(),
|
|
471
|
+
tier: Tier::Semantic,
|
|
472
|
+
content: "hello".to_string(),
|
|
473
|
+
metadata: serde_json::json!({"k": "v"}),
|
|
474
|
+
created_at: chrono::Utc::now(),
|
|
475
|
+
};
|
|
476
|
+
let dto = memory_to_dto(memory.clone());
|
|
477
|
+
assert_eq!(dto.id, id.to_string());
|
|
478
|
+
assert_eq!(dto.tenant_id, "t1");
|
|
479
|
+
assert_eq!(dto.tier, "semantic");
|
|
480
|
+
assert_eq!(dto.content, "hello");
|
|
481
|
+
assert_eq!(dto.metadata, serde_json::json!({"k": "v"}));
|
|
482
|
+
assert_eq!(dto.created_at, memory.created_at.to_rfc3339());
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#[test]
|
|
486
|
+
fn forget_proof_to_dto_maps_every_field() {
|
|
487
|
+
let event_id = Uuid::new_v4();
|
|
488
|
+
let dto = forget_proof_to_dto(ForgetProof {
|
|
489
|
+
audit_event_id: event_id,
|
|
490
|
+
hash: "deadbeef".to_string(),
|
|
491
|
+
});
|
|
492
|
+
assert_eq!(dto.audit_event_id, event_id.to_string());
|
|
493
|
+
assert_eq!(dto.hash, "deadbeef");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#[test]
|
|
497
|
+
fn audit_event_to_dto_maps_every_field_including_none_prev_hash() {
|
|
498
|
+
let event = AuditEvent::next("t1", AuditOperation::Store, Uuid::new_v4(), None);
|
|
499
|
+
let dto = audit_event_to_dto(event.clone());
|
|
500
|
+
assert_eq!(dto.id, event.id.to_string());
|
|
501
|
+
assert_eq!(dto.tenant_id, "t1");
|
|
502
|
+
assert_eq!(dto.operation, "store");
|
|
503
|
+
assert_eq!(dto.memory_id, event.memory_id.to_string());
|
|
504
|
+
assert_eq!(dto.at, event.at.to_rfc3339());
|
|
505
|
+
assert_eq!(dto.prev_hash, None);
|
|
506
|
+
assert_eq!(dto.hash, event.hash);
|
|
507
|
+
}
|
|
508
|
+
}
|