@volter/twin-pinecone 0.1.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.
@@ -0,0 +1,491 @@
1
+ // Pinecone API twin REQUEST HANDLER — a v1 slice of the Pinecone vector-database surface, backed
2
+ // by the event/action-log kernel (@volter/world-core). Contract:
3
+ // handlePineconeTwinRequest({ method, path, body, host?, index?, root, readOnly }) ->
4
+ // { status, body, headers }
5
+ //
6
+ // SOURCE OF TRUTH: authored against the real, first-party `@pinecone-database/pinecone` npm
7
+ // package's OWN generated TypeScript-fetch client (the SDK's `dist/pinecone-generated-ts-fetch/
8
+ // db_control` and `db_data` model files were fetched — via `npm pack`, an allowed unauthenticated
9
+ // read — and read directly during this build; see spec-sources.json) — every route path, JSON
10
+ // field name (all camelCase on the wire: `topK`, `includeValues`, `includeMetadata`,
11
+ // `sparseValues`, `upsertedCount`, ...), and the two-plane error-envelope split below is grounded
12
+ // against that generated client, NOT guessed. A handful of items the generated client doesn't
13
+ // settle (exact index create/delete status codes, euclidean's exact wire score, query-by-id
14
+ // self-inclusion, control-plane error shape) were additionally checked against Pinecone's public
15
+ // docs (docs.pinecone.io, unauthenticated GET) and are annotated doc-UNVERIFIED inline where the
16
+ // docs didn't resolve them — see spec-sources.json's `pinecone` entry for the full grounded-vs-
17
+ // doc-UNVERIFIED breakdown.
18
+ //
19
+ // HOST-SPLIT (control vs data plane), grounded from the SDK: `new Pinecone({ controllerHostUrl
20
+ // })` targets the CONTROL plane (index/collection management — `/indexes`, `/collections`); a
21
+ // per-index DATA plane host is minted at index-create time (`IndexModel.host`) and targeted via
22
+ // `pc.index({ name, host })` (verified in the SDK's own `data/index.js`: an explicit `host`
23
+ // bypasses the SDK's own `IndexHostSingleton.getHostUrl` describeIndex lookup entirely — the
24
+ // literal doc-comment example is `const index = pc.index({ host: indexModel.host })`). This twin
25
+ // mints a real (if synthetic-looking) per-index hostname (`mintHost`) and a single request router
26
+ // (`routePineconeSurface`) decides control-vs-data purely from the effective request host (or, as
27
+ // a path fallback, whether the path starts with `/indexes` or `/collections`) — see
28
+ // pinecone-server.ts's header comment for how an unmodified SDK's requests, which the SDK's
29
+ // `normalizeUrl` will always try to dial as a real DNS host, are routed to this ONE local
30
+ // twin process without ever touching the real network (a `fetchApi` override, not DNS).
31
+ //
32
+ // State lives ENTIRELY in the kernel action log: writes go through `applyTwinWrite`, reads are
33
+ // the projection (`projectResources`). There is NO Map/array side-store — the projection over the
34
+ // append-only log is the single source of truth (D1). No real Pinecone is ever contacted.
35
+ //
36
+ // WHAT THIS TWIN DOES (D2): the query is REAL brute-force exact top-K (pinecone-similarity.ts), a
37
+ // faithful SUPERSET of Pinecone's approximate-NN recall, not a lesser stub. Indexes are marked
38
+ // `ready` instantly and `indexFullness` is always 0. The integrated-embedding surface
39
+ // (`createIndexForModel`, `/records/.../upsert|search`) is filed as
40
+ // `pinecone.records.integrated_surface`; callers supply their own vectors today.
41
+ //
42
+ // Unmodeled route/op returns Pinecone's real error envelope for that plane (D2 — never a
43
+ // fabricated success). readOnly rejects WRITES with 405 — gated per-operation (not by HTTP verb:
44
+ // `POST /query` and `POST /describe_index_stats` are read-shaped POSTs and must stay allowed in
45
+ // readOnly mode, mirroring how this repo's POST-transport GraphQL packs gate readOnly per-
46
+ // mutation rather than by HTTP method — see linear-state.ts).
47
+ import { createHash } from 'node:crypto';
48
+ import { applyTwinWrite, projectResources } from '@volter/world-core';
49
+ import { matchesFilter } from "./pinecone-filter.js";
50
+ import { scoreVector, sortByScore } from "./pinecone-similarity.js";
51
+ const SERVICE = 'pinecone';
52
+ const DEFAULT_NAMESPACE = '';
53
+ export const PINECONE_RESOURCE_TYPES = ['index', 'collection', 'vector'];
54
+ // ── host-split router ──────────────────────────────────────────────────────────────────────
55
+ const DATA_HOST_SUFFIX = '.svc.twin.pinecone.io';
56
+ /** Deterministic per-INDEX synthetic data-plane host — stable across calls, never a real
57
+ * resolvable DNS name (this twin is only ever reached via pinecone-server.ts's fetchApi-level
58
+ * redirect, so real-network resolvability is never required — see file header).
59
+ *
60
+ * R9 (serve-path determinism) AT THE RESOURCE LEVEL: the minted host is a function of the INDEX
61
+ * NAME and of nothing else. It deliberately does NOT hash the world `root`: the root is a
62
+ * filesystem PATH, so folding it in made `IndexModel.host` — a field every list/describe serves
63
+ * and every SDK dials — a function of WHERE the world happens to live, and two identical worlds
64
+ * under different directories served different hosts (found 2026-09-02 by the R9 replay sweep,
65
+ * the algolia `mintObjectId` defect in its second form). Uniqueness per root was never the
66
+ * property that mattered: a host only ever has to round-trip back to its index name through
67
+ * `recoverIndexFromHost` within ONE world, and index names are already unique within one. */
68
+ export function mintHost(indexName) {
69
+ const h = createHash('sha256').update(`pinecone-twin-host:${indexName}`).digest('hex').slice(0, 8);
70
+ return `${indexName}-${h}${DATA_HOST_SUFFIX}`;
71
+ }
72
+ /** Recover an index name from a minted data-plane host (strip scheme/port, then the known
73
+ * suffix and trailing `-<hash8>`). Returns undefined for the control host or an unrecognized one. */
74
+ export function recoverIndexFromHost(host) {
75
+ if (!host)
76
+ return undefined;
77
+ const bare = host.replace(/^https?:\/\//, '').split(':')[0];
78
+ if (!bare.endsWith(DATA_HOST_SUFFIX))
79
+ return undefined;
80
+ const stem = bare.slice(0, -DATA_HOST_SUFFIX.length);
81
+ const m = stem.match(/^(.*)-([0-9a-f]{8})$/);
82
+ return m ? m[1] : undefined;
83
+ }
84
+ /** Decide control- vs data-plane purely from the effective host (path is a fallback for the
85
+ * twin's own verifies that don't bother setting a host at all). */
86
+ export function routePineconeSurface(req) {
87
+ const host = req.host;
88
+ const path = req.path.split('?')[0] ?? '/';
89
+ if (host === 'api.pinecone.io' || (host !== undefined && host.startsWith('api.pinecone.io:')))
90
+ return 'control';
91
+ if (host !== undefined && recoverIndexFromHost(host))
92
+ return 'data';
93
+ // No host, or a host that doesn't parse as a minted data-plane host — fall back to a
94
+ // path-shaped guess: this is what lets the twin's own verifies (and the capability manifest's
95
+ // `withRoot` helper) drive data-plane routes purely via the explicit `index` field, with no host
96
+ // round-trip at all (spec §6's documented fallback), while `/indexes`/`/collections` still
97
+ // resolve as control even with no host set.
98
+ if (path.startsWith('/indexes') || path.startsWith('/collections') || path === '/')
99
+ return 'control';
100
+ return 'data';
101
+ }
102
+ // ── error envelopes (two-plane split, D2/⚠2) ─────────────────────────────────────────────────
103
+ // DATA plane (gRPC-gateway): grounded verbatim against docs.pinecone.io's Query reference (fetched
104
+ // during this build) — `{code:int, message:string, details:[{typeUrl,value}]}` — codes per the
105
+ // gRPC-gateway convention: 3=INVALID_ARGUMENT->400, 5=NOT_FOUND->404, 6=ALREADY_EXISTS->409.
106
+ function dataError(status, code, message) {
107
+ return { status, body: { code, message, details: [] } };
108
+ }
109
+ // CONTROL plane: doc-UNVERIFIED (the fetched docs.pinecone.io error-handling page 404'd during
110
+ // this build's read-only pass) — modeled per the build spec's own grounding pass:
111
+ // `{error:{code, message}, status}`, a DIFFERENT shape from the data plane. Filed as
112
+ // `pinecone.errors.control_plane_shape` (todo) for full parity once independently confirmed.
113
+ function controlError(status, code, message) {
114
+ return { status, body: { error: { code, message }, status } };
115
+ }
116
+ function readOnlyRejection() {
117
+ return { status: 405, body: { message: 'twin is read-only; omit readOnly to accept writes' } };
118
+ }
119
+ // ── body / helpers ────────────────────────────────────────────────────────────────────────────
120
+ function parseBody(body) {
121
+ if (!body)
122
+ return {};
123
+ try {
124
+ const v = JSON.parse(body);
125
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
126
+ }
127
+ catch {
128
+ return {};
129
+ }
130
+ }
131
+ function nowIso(occurredAt) {
132
+ return occurredAt ?? new Date().toISOString();
133
+ }
134
+ // ── kernel subject ids (type-prefixed; D1) ──────────────────────────────────────────────────
135
+ function indexKey(name) {
136
+ return `index:${name}`;
137
+ }
138
+ function collectionKey(name) {
139
+ return `collection:${name}`;
140
+ }
141
+ function vectorKey(index, namespace, id) {
142
+ return `vector:${index}::${namespace}::${id}`;
143
+ }
144
+ function parseVectorSuffix(suffix) {
145
+ const parts = suffix.split('::');
146
+ if (parts.length !== 3)
147
+ return undefined;
148
+ return { index: parts[0], namespace: parts[1], id: parts[2] };
149
+ }
150
+ function rowsOfType(type, root) {
151
+ const prefix = `${type}:`;
152
+ return projectResources(SERVICE, root).filter((r) => r.type === type && r.id.startsWith(prefix) && r._deleted !== true);
153
+ }
154
+ function getRowById(type, subjectId, root) {
155
+ return projectResources(SERVICE, root).find((r) => r.type === type && r.id === subjectId && r._deleted !== true);
156
+ }
157
+ async function applyWrite(subjectType, subjectId, fields, operation, req) {
158
+ const { resource } = await applyTwinWrite(SERVICE, { operation, subjectType, subjectId, fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } }, req.root);
159
+ return resource;
160
+ }
161
+ // ── object views (mirror the SDK's generated model field names exactly) ────────────────────────
162
+ function indexView(r) {
163
+ // WIRE field names, grounded against the SDK's generated IndexModel (De)Serializer
164
+ // (pinecone-generated-ts-fetch/db_control/models/IndexModel.js): most fields are camelCase, but
165
+ // `deletion_protection` and `vector_type` are SNAKE_CASE on the wire — a genuine, easy-to-miss
166
+ // inconsistency within the control plane itself (the data plane is uniformly camelCase — see
167
+ // pinecone-similarity.ts/query wire shapes). Internal kernel row field names stay camelCase
168
+ // (this pack's own choice); only this view's OUTPUT keys must match the wire exactly.
169
+ return {
170
+ name: r.name,
171
+ dimension: r.dimension ?? null,
172
+ metric: r.metric,
173
+ host: r.host,
174
+ spec: r.spec,
175
+ status: { ready: r.ready ?? true, state: r.state ?? 'Ready' },
176
+ deletion_protection: r.deletionProtection ?? 'disabled',
177
+ tags: r.tags ?? {},
178
+ vector_type: r.vectorType ?? 'dense',
179
+ };
180
+ }
181
+ function collectionView(r) {
182
+ // WIRE: `vector_count` is snake_case (CollectionModel.js); everything else here is camelCase-
183
+ // identical to its single-word form.
184
+ return {
185
+ name: r.name,
186
+ size: r.size ?? 0,
187
+ status: r.status ?? 'Ready',
188
+ dimension: r.dimension ?? null,
189
+ vector_count: r.vectorCount ?? 0,
190
+ environment: r.environment ?? 'twin-local',
191
+ };
192
+ }
193
+ function vectorView(index, namespace, id, r) {
194
+ return { id, values: r.values ?? [], metadata: r.metadata ?? {} };
195
+ }
196
+ // ── route handler ────────────────────────────────────────────────────────────────────────────
197
+ export async function handlePineconeTwinRequest(req) {
198
+ const method = req.method.toUpperCase();
199
+ const [rawPath, rawQuery] = req.path.split('?');
200
+ const path = (rawPath ?? '/').replace(/\/+$/, '') || '/';
201
+ const query = new URLSearchParams(rawQuery ?? '');
202
+ const body = parseBody(req.body);
203
+ const seg = path.replace(/^\/+/, '').split('/').filter(Boolean);
204
+ const idAt = (i) => decodeURIComponent(seg[i] ?? '');
205
+ const surface = routePineconeSurface({ host: req.host, path });
206
+ if (path === '/' || seg.length === 0) {
207
+ return { status: 200, body: { service: 'pinecone', object: 'twin' } };
208
+ }
209
+ // ══════════════════════════════ CONTROL PLANE (api.pinecone.io) ═══════════════════════════
210
+ if (surface === 'control') {
211
+ // ── indexes ──
212
+ if (seg[0] === 'indexes') {
213
+ if (seg.length === 1 && method === 'POST') {
214
+ if (req.readOnly)
215
+ return readOnlyRejection();
216
+ const name = typeof body.name === 'string' ? body.name : undefined;
217
+ // The real CreateIndexRequest schema makes `dimension` optional (only omittable for a
218
+ // model-backed / integrated-embedding index — `createIndexForModel`, filed as
219
+ // `pinecone.records.integrated_surface`). Since this twin
220
+ // never infers a dimension from a model, it requires an explicit one on every create.
221
+ const dimension = typeof body.dimension === 'number' ? body.dimension : undefined;
222
+ if (!name || !dimension)
223
+ return controlError(400, 'INVALID_ARGUMENT', 'name and dimension are required to create an index');
224
+ if (getRowById('index', indexKey(name), req.root))
225
+ return controlError(409, 'ALREADY_EXISTS', `Resource ${name} already exists`);
226
+ const metric = typeof body.metric === 'string' ? body.metric : 'cosine';
227
+ // WIRE keys are snake_case here (CreateIndexRequestToJSON) — see indexView's header note.
228
+ const vectorType = typeof body.vector_type === 'string' ? body.vector_type : 'dense';
229
+ const spec = body.spec ?? { serverless: { cloud: 'aws', region: 'us-east-1' } };
230
+ const deletionProtection = typeof body.deletion_protection === 'string' ? body.deletion_protection : 'disabled';
231
+ const tags = body.tags ?? {};
232
+ const host = mintHost(name);
233
+ await applyWrite('index', indexKey(name), { name, dimension, metric, vectorType, spec, deletionProtection, tags, host, ready: true, state: 'Ready' }, 'index.create', req);
234
+ const row = getRowById('index', indexKey(name), req.root);
235
+ return { status: 201, body: indexView(row) };
236
+ }
237
+ if (seg.length === 1 && method === 'GET') {
238
+ return { status: 200, body: { indexes: rowsOfType('index', req.root).map(indexView) } };
239
+ }
240
+ if (seg.length === 2 && method === 'GET') {
241
+ const row = getRowById('index', indexKey(idAt(1)), req.root);
242
+ if (!row)
243
+ return controlError(404, 'NOT_FOUND', `Index ${idAt(1)} not found`);
244
+ return { status: 200, body: indexView(row) };
245
+ }
246
+ if (seg.length === 2 && method === 'PATCH') {
247
+ if (req.readOnly)
248
+ return readOnlyRejection();
249
+ const row = getRowById('index', indexKey(idAt(1)), req.root);
250
+ if (!row)
251
+ return controlError(404, 'NOT_FOUND', `Index ${idAt(1)} not found`);
252
+ // Dimension is immutable post-create (real-API invariant); reject an attempt to change it.
253
+ if (typeof body.dimension === 'number' && body.dimension !== row.dimension) {
254
+ return controlError(400, 'INVALID_ARGUMENT', 'dimension cannot be changed after index creation');
255
+ }
256
+ const fields = {};
257
+ // WIRE key is snake_case here (ConfigureIndexRequestToJSON) — see indexView's header note.
258
+ if (body.deletion_protection !== undefined)
259
+ fields.deletionProtection = body.deletion_protection;
260
+ if (body.tags !== undefined)
261
+ fields.tags = { ...(row.tags ?? {}), ...body.tags };
262
+ if (body.spec !== undefined)
263
+ fields.spec = { ...(row.spec ?? {}), ...body.spec };
264
+ await applyWrite('index', indexKey(idAt(1)), fields, 'index.configure', req);
265
+ const updated = getRowById('index', indexKey(idAt(1)), req.root);
266
+ return { status: 200, body: indexView(updated) };
267
+ }
268
+ if (seg.length === 2 && method === 'DELETE') {
269
+ if (req.readOnly)
270
+ return readOnlyRejection();
271
+ const row = getRowById('index', indexKey(idAt(1)), req.root);
272
+ if (!row)
273
+ return controlError(404, 'NOT_FOUND', `Index ${idAt(1)} not found`);
274
+ await applyWrite('index', indexKey(idAt(1)), { _deleted: true }, 'index.delete', req);
275
+ // Real Pinecone index deletion is asynchronous; 202 Accepted is the conventional REST
276
+ // reading (doc-UNVERIFIED exact code — ⚠6, spec-sources.json) — this twin deletes
277
+ // synchronously so a follow-up describe/list reflects it immediately either way.
278
+ return { status: 202, body: null };
279
+ }
280
+ }
281
+ // ── collections ── (a collection is a named, curated snapshot of an existing index; full
282
+ // point-in-time snapshot semantics are `pinecone.collections.from_index_snapshot` (todo) —
283
+ // this v1 slice models create/list/describe/delete as kernel-tracked metadata records.)
284
+ if (seg[0] === 'collections') {
285
+ if (seg.length === 1 && method === 'POST') {
286
+ if (req.readOnly)
287
+ return readOnlyRejection();
288
+ const name = typeof body.name === 'string' ? body.name : undefined;
289
+ const source = typeof body.source === 'string' ? body.source : undefined;
290
+ if (!name || !source)
291
+ return controlError(400, 'INVALID_ARGUMENT', 'name and source (an existing index name) are required to create a collection');
292
+ const sourceIndex = getRowById('index', indexKey(source), req.root);
293
+ if (!sourceIndex)
294
+ return controlError(404, 'NOT_FOUND', `Index ${source} not found`);
295
+ if (getRowById('collection', collectionKey(name), req.root))
296
+ return controlError(409, 'ALREADY_EXISTS', `Resource ${name} already exists`);
297
+ await applyWrite('collection', collectionKey(name), { name, dimension: sourceIndex.dimension, vectorCount: 0, size: 0, status: 'Ready', environment: 'twin-local' }, 'collection.create', req);
298
+ const row = getRowById('collection', collectionKey(name), req.root);
299
+ return { status: 201, body: collectionView(row) };
300
+ }
301
+ if (seg.length === 1 && method === 'GET') {
302
+ return { status: 200, body: { collections: rowsOfType('collection', req.root).map(collectionView) } };
303
+ }
304
+ if (seg.length === 2 && method === 'GET') {
305
+ const row = getRowById('collection', collectionKey(idAt(1)), req.root);
306
+ if (!row)
307
+ return controlError(404, 'NOT_FOUND', `Collection ${idAt(1)} not found`);
308
+ return { status: 200, body: collectionView(row) };
309
+ }
310
+ if (seg.length === 2 && method === 'DELETE') {
311
+ if (req.readOnly)
312
+ return readOnlyRejection();
313
+ const row = getRowById('collection', collectionKey(idAt(1)), req.root);
314
+ if (!row)
315
+ return controlError(404, 'NOT_FOUND', `Collection ${idAt(1)} not found`);
316
+ await applyWrite('collection', collectionKey(idAt(1)), { _deleted: true }, 'collection.delete', req);
317
+ return { status: 202, body: null };
318
+ }
319
+ }
320
+ return controlError(404, 'NOT_FOUND', `Unrecognized control-plane route: ${method} ${path}`);
321
+ }
322
+ // ════════════════════════════════ DATA PLANE (per-index minted host) ══════════════════════
323
+ const indexName = req.index ?? recoverIndexFromHost(req.host);
324
+ if (!indexName)
325
+ return dataError(404, 5, 'Could not resolve an index for this request host');
326
+ const indexRow = getRowById('index', indexKey(indexName), req.root);
327
+ if (!indexRow)
328
+ return dataError(404, 5, `Index ${indexName} not found`);
329
+ const dimension = typeof indexRow.dimension === 'number' ? indexRow.dimension : undefined;
330
+ const metric = indexRow.metric ?? 'cosine';
331
+ function vectorRowsIn(namespace) {
332
+ const prefix = `vector:${indexName}::${namespace}::`;
333
+ return rowsOfType('vector', req.root)
334
+ .filter((r) => r.id.startsWith(prefix))
335
+ .map((r) => {
336
+ const parsed = parseVectorSuffix(r.id.slice('vector:'.length));
337
+ return { id: parsed.id, values: r.values ?? [], metadata: r.metadata ?? {} };
338
+ });
339
+ }
340
+ if (seg[0] === 'vectors' && seg[1] === 'upsert' && seg.length === 2 && method === 'POST') {
341
+ if (req.readOnly)
342
+ return readOnlyRejection();
343
+ const vectors = Array.isArray(body.vectors) ? body.vectors : [];
344
+ const namespace = typeof body.namespace === 'string' ? body.namespace : DEFAULT_NAMESPACE;
345
+ for (const v of vectors) {
346
+ const values = Array.isArray(v.values) ? v.values : [];
347
+ if (dimension !== undefined && values.length !== dimension) {
348
+ return dataError(400, 3, `Vector dimension ${values.length} does not match the dimension of the index ${dimension}`);
349
+ }
350
+ }
351
+ for (const v of vectors) {
352
+ const id = String(v.id ?? '');
353
+ if (!id)
354
+ continue;
355
+ await applyWrite('vector', vectorKey(indexName, namespace, id), { values: v.values ?? [], metadata: v.metadata ?? {} }, 'vector.upsert', req);
356
+ }
357
+ return { status: 200, body: { upsertedCount: vectors.length } };
358
+ }
359
+ if (seg[0] === 'vectors' && seg[1] === 'fetch' && seg.length === 2 && method === 'GET') {
360
+ const ids = (query.get('ids') ?? '').split(',').map((s) => s.trim()).filter(Boolean);
361
+ const namespace = query.get('namespace') ?? DEFAULT_NAMESPACE;
362
+ const vectors = {};
363
+ for (const id of ids) {
364
+ const row = getRowById('vector', vectorKey(indexName, namespace, id), req.root);
365
+ // Unknown ids are simply OMITTED from the map (not a 404) — the docs.pinecone.io fetch-data
366
+ // guide describes the response as "a map between the fetched ids and the fetched vectors"
367
+ // with no documented error path for a missing id; doc-UNVERIFIED but structurally the
368
+ // natural reading of a map-shaped response, not a fabricated success (⚠5).
369
+ if (row)
370
+ vectors[id] = vectorView(indexName, namespace, id, row);
371
+ }
372
+ return { status: 200, body: { vectors, namespace } };
373
+ }
374
+ if (seg[0] === 'vectors' && seg[1] === 'update' && seg.length === 2 && method === 'POST') {
375
+ if (req.readOnly)
376
+ return readOnlyRejection();
377
+ const id = String(body.id ?? '');
378
+ const namespace = typeof body.namespace === 'string' ? body.namespace : DEFAULT_NAMESPACE;
379
+ if (!id)
380
+ return dataError(400, 3, 'id is required');
381
+ const existing = getRowById('vector', vectorKey(indexName, namespace, id), req.root);
382
+ if (existing) {
383
+ const fields = {};
384
+ if (body.values !== undefined)
385
+ fields.values = body.values;
386
+ if (body.setMetadata !== undefined)
387
+ fields.metadata = { ...(existing.metadata ?? {}), ...body.setMetadata };
388
+ await applyWrite('vector', vectorKey(indexName, namespace, id), fields, 'vector.update', req);
389
+ }
390
+ return { status: 200, body: {} };
391
+ }
392
+ if (seg[0] === 'vectors' && seg[1] === 'delete' && seg.length === 2 && method === 'POST') {
393
+ if (req.readOnly)
394
+ return readOnlyRejection();
395
+ const namespace = typeof body.namespace === 'string' ? body.namespace : DEFAULT_NAMESPACE;
396
+ if (body.deleteAll === true) {
397
+ // deleteAll only clears the TARGETED namespace, never the whole index (⚠ vectors.delete_all).
398
+ for (const v of vectorRowsIn(namespace))
399
+ await applyWrite('vector', vectorKey(indexName, namespace, v.id), { _deleted: true }, 'vector.delete', req);
400
+ }
401
+ else if (Array.isArray(body.ids)) {
402
+ for (const id of body.ids)
403
+ await applyWrite('vector', vectorKey(indexName, namespace, id), { _deleted: true }, 'vector.delete', req);
404
+ }
405
+ else if (body.filter !== undefined) {
406
+ // Real metadata-filter-driven delete (⚠ vectors.delete_by_filter) — never a name/keyword match.
407
+ for (const v of vectorRowsIn(namespace)) {
408
+ if (matchesFilter(v.metadata, body.filter)) {
409
+ await applyWrite('vector', vectorKey(indexName, namespace, v.id), { _deleted: true }, 'vector.delete', req);
410
+ }
411
+ }
412
+ }
413
+ return { status: 200, body: {} };
414
+ }
415
+ if (seg[0] === 'vectors' && seg[1] === 'list' && seg.length === 2 && method === 'GET') {
416
+ const namespace = query.get('namespace') ?? DEFAULT_NAMESPACE;
417
+ const prefix = query.get('prefix') ?? '';
418
+ const limit = Number(query.get('limit') ?? '100') || 100;
419
+ const items = vectorRowsIn(namespace).filter((v) => v.id.startsWith(prefix)).slice(0, limit);
420
+ return { status: 200, body: { vectors: items.map((v) => ({ id: v.id })), namespace, pagination: { next: null } } };
421
+ }
422
+ if (path === '/query' && method === 'POST') {
423
+ const namespace = typeof body.namespace === 'string' ? body.namespace : DEFAULT_NAMESPACE;
424
+ const topK = typeof body.topK === 'number' ? body.topK : 10;
425
+ const includeValues = body.includeValues === true;
426
+ const includeMetadata = body.includeMetadata === true;
427
+ const filter = body.filter;
428
+ let queryVector;
429
+ let selfId;
430
+ if (Array.isArray(body.vector)) {
431
+ queryVector = body.vector;
432
+ }
433
+ else if (typeof body.id === 'string') {
434
+ selfId = body.id;
435
+ const self = getRowById('vector', vectorKey(indexName, namespace, body.id), req.root);
436
+ queryVector = self ? (self.values ?? []) : undefined;
437
+ }
438
+ if (!queryVector)
439
+ return { status: 200, body: { matches: [], namespace } };
440
+ const candidates = vectorRowsIn(namespace)
441
+ .filter((v) => v.id !== selfId) // query-by-id EXCLUDES the query vector itself (⚠ query.by_id
442
+ // self-inclusion — not settled by the fetched docs; modeled on the common ANN convention
443
+ // that a nearest-neighbor search never returns the query point as its own neighbor;
444
+ // doc-UNVERIFIED, recorded in spec-sources.json).
445
+ .filter((v) => matchesFilter(v.metadata, filter))
446
+ .map((v) => ({ id: v.id, score: scoreVector(metric, queryVector, v.values), values: v.values, metadata: v.metadata }));
447
+ const ranked = sortByScore(metric, candidates).slice(0, Math.max(0, topK));
448
+ const matches = ranked.map((m) => ({
449
+ id: m.id,
450
+ score: m.score,
451
+ ...(includeValues ? { values: m.values } : {}),
452
+ ...(includeMetadata ? { metadata: m.metadata } : {}),
453
+ }));
454
+ return { status: 200, body: { matches, namespace } };
455
+ }
456
+ if (path === '/describe_index_stats' && method === 'POST') {
457
+ const allVectors = rowsOfType('vector', req.root).filter((r) => r.id.startsWith(`vector:${indexName}::`));
458
+ const namespaces = {};
459
+ for (const r of allVectors) {
460
+ const parsed = parseVectorSuffix(r.id.slice('vector:'.length));
461
+ if (!parsed)
462
+ continue;
463
+ namespaces[parsed.namespace] = { vectorCount: (namespaces[parsed.namespace]?.vectorCount ?? 0) + 1 };
464
+ }
465
+ return {
466
+ status: 200,
467
+ body: {
468
+ namespaces,
469
+ dimension: dimension ?? null,
470
+ // No real pod/serverless infrastructure is provisioned — indexFullness has no organic
471
+ // meaning here, so it is
472
+ // always 0 rather than a fabricated non-zero figure.
473
+ indexFullness: 0,
474
+ totalVectorCount: allVectors.length,
475
+ },
476
+ };
477
+ }
478
+ return dataError(404, 5, `Unrecognized data-plane route: ${method} ${path}`);
479
+ }
480
+ // Endpoint inventory used by the conformance snapshot.
481
+ export function pineconeTwinSnapshot() {
482
+ return {
483
+ resourceTypes: PINECONE_RESOURCE_TYPES,
484
+ implementedEndpoints: [
485
+ 'POST /indexes', 'GET /indexes', 'GET /indexes/:name', 'PATCH /indexes/:name', 'DELETE /indexes/:name',
486
+ 'POST /collections', 'GET /collections', 'GET /collections/:name', 'DELETE /collections/:name',
487
+ 'POST /vectors/upsert', 'GET /vectors/fetch', 'POST /vectors/update', 'POST /vectors/delete', 'GET /vectors/list',
488
+ 'POST /query', 'POST /describe_index_stats',
489
+ ],
490
+ };
491
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@volter/twin-pinecone",
3
+ "version": "0.1.0",
4
+ "description": "Local Pinecone twin for the vector-database surface: index/collection management (control plane), vector upsert/fetch/update/delete, REAL brute-force cosine/dotproduct/euclidean similarity query with a REAL Mongo-style metadata-filter evaluator, namespaces, and host-split control/data-plane routing, built on @volter/world-core.",
5
+ "author": "Volter (https://github.com/volter-ai)",
6
+ "license": "Apache-2.0",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE",
11
+ "!**/*.test.ts",
12
+ "dist"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/volter-ai/twin.git",
17
+ "directory": "packages/twin/pinecone"
18
+ },
19
+ "homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/pinecone#readme",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/src/index.d.ts",
24
+ "default": "./dist/src/index.js"
25
+ }
26
+ },
27
+ "bin": {
28
+ "world-pinecone": "dist/src/cli.js"
29
+ },
30
+ "scripts": {
31
+ "test": "bun test src/*.test.ts",
32
+ "typecheck": "tsc --noEmit",
33
+ "build": "node ../../../scripts/publish/build.mjs",
34
+ "prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
35
+ "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
36
+ },
37
+ "peerDependencies": {
38
+ "@volter/world-core": "2.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "^1.2.20",
42
+ "@types/node": "^24.0.0",
43
+ "@volter/world-core": "2.0.0",
44
+ "@volter/world-tooling": "0.1.0",
45
+ "@pinecone-database/pinecone": "^8.0.0",
46
+ "typescript": "^5.9.0"
47
+ },
48
+ "engines": {
49
+ "node": ">=22.3"
50
+ }
51
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ // world-pinecone CLI: serve the KERNEL-BACKED Pinecone API twin, or run conformance. State
4
+ // lives in the @volter/world-core action log (no in-memory side-store). Conformance is dev-only +
5
+ // lazy-imported so the bin runs without @volter/world-tooling (E2).
6
+ import { hasFlag, optionValue } from '@volter/world-core/args';
7
+ import { createPineconeTwinServer } from './pinecone-server.ts';
8
+
9
+ const [cmd, ...rest] = process.argv.slice(2);
10
+ const port = Number(optionValue(rest, '--port', String(process.env.PORT ?? '0'))) || undefined;
11
+ const root = optionValue(rest, '--root') || undefined;
12
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
13
+
14
+ if (cmd === 'serve' || cmd === undefined) {
15
+ const s = await createPineconeTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
16
+ process.stdout.write(`pinecone twin (vector database: indexes/collections/vectors + real similarity query)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
17
+ await keepProcessAlive();
18
+ } else if (cmd === 'conformance') {
19
+ const { checkPineconeConformance } = await import('./pinecone-conformance.ts');
20
+ const report = checkPineconeConformance();
21
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
22
+ if (!report.ok) process.exitCode = 1;
23
+ } else {
24
+ process.stdout.write('Usage: world-pinecone serve|conformance [--port N] [--root DIR] [--read-only]\n');
25
+ }