@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,130 @@
1
+ // Pinecone CONNECTOR — the live-vendor pull path over an INJECTED client (the auth boundary; a
2
+ // fake in tests, the real `@pinecone-database/pinecone` SDK in prod). The pack imports NO SDK and
3
+ // holds NO key at runtime — only this file's *type* shapes describe what a real client call would
4
+ // return; the actual SDK is a devDependency imported ONLY in *.test.ts (architecture.test.ts).
5
+ //
6
+ // PULL (real -> twin): fetch real indexes + vectors via the injected client, map them to
7
+ // SyncResource[], and fold them into the event log via `syncPull` (shadow-diff dedup, so a
8
+ // re-pull of identical state is a no-op). Kernel subject ids are TYPE-PREFIXED to stay
9
+ // collision-safe (mirrors pinecone-twin.ts).
10
+ //
11
+ // Vectors are genuinely HANDLE-DRIVEN, the same honest shape fal's connector uses (see fal-
12
+ // connector.ts / pull-audit.json's fal notes): Pinecone's real SDK has NO cross-index "list every
13
+ // vector in my project" endpoint — `Index.listPaginated()` is scoped to one already-known index +
14
+ // namespace. So `PineconeLikeClient.listVectors` is this connector's OWN pull-source shape (a
15
+ // real production implementation loops `pc.listIndexes()` then `pc.index(name).listPaginated()`
16
+ // per namespace and flattens the result into exactly this shape) rather than a literal single
17
+ // vendor endpoint — same honesty framing as fal's `syncFalFromReal`.
18
+ //
19
+ // A pulled index is re-minted a LOCAL data-plane host (`mintHost`) rather than keeping whatever
20
+ // host the real vendor reported — the twin's own data-plane router only ever recognizes its own
21
+ // minted hosts (pinecone-twin.ts's `recoverIndexFromHost`), so a pulled index must get one too to
22
+ // stay queryable through this twin.
23
+ import { syncPull } from '@volter/world-core';
24
+ import type { SyncResource } from '@volter/world-core';
25
+ import { mintHost } from './pinecone-twin.ts';
26
+ //
27
+ // ── The client-side RATE BUDGET is not optional here ────────────────────────────────────────
28
+ // Pinecone publishes a detailed DATA-plane limits table but NO row for the CONTROL-plane listIndexes
29
+ // this connector calls, and `listVectors` here is a cross-index, per-namespace paginated aggregation
30
+ // rather than one vendor endpoint.
31
+ // Every entrypoint below GUARDS the injected client before touching it (`guardPineconeClient`, which is
32
+ // idempotent — a caller who already wrapped is not double-charged, a caller who forgot is protected
33
+ // anyway); there is deliberately no option that turns the budget off. See pinecone-budget.ts.
34
+ import { pineconeBudgetOf, guardPineconeClient, type PineconeBudgetedOptions } from './pinecone-budget.ts';
35
+
36
+ const SERVICE = 'pinecone';
37
+
38
+ export type { PineconeBudgetedOptions };
39
+
40
+ export type PineconeRealIndex = {
41
+ name: string;
42
+ dimension?: number | null;
43
+ metric?: string | null;
44
+ spec?: unknown;
45
+ deletionProtection?: string | null;
46
+ tags?: Record<string, string> | null;
47
+ vectorType?: string | null;
48
+ };
49
+ export type PineconeRealVector = {
50
+ index: string;
51
+ namespace?: string | null;
52
+ id: string;
53
+ values?: number[] | null;
54
+ metadata?: Record<string, unknown> | null;
55
+ };
56
+
57
+ // The injected client exposes the minimal subset the connector calls; a real Pinecone-backed
58
+ // implementation is structurally assignable (see file header for why `listVectors` is a
59
+ // connector-owned aggregation shape, not a literal SDK method name).
60
+ export interface PineconeLikeClient {
61
+ listIndexes?: () => Promise<{ indexes: PineconeRealIndex[] }>;
62
+ listVectors?: () => Promise<{ vectors: PineconeRealVector[] }>;
63
+ }
64
+
65
+ function indexKey(name: string): string {
66
+ return `index:${name}`;
67
+ }
68
+ function vectorKey(index: string, namespace: string, id: string): string {
69
+ return `vector:${index}::${namespace}::${id}`;
70
+ }
71
+
72
+ // ── PULL mappers (real Pinecone object → SyncResource) ─────────────────────────────────────
73
+ export function mapIndex(m: PineconeRealIndex): SyncResource {
74
+ return {
75
+ type: 'index',
76
+ id: indexKey(m.name),
77
+ fields: {
78
+ name: m.name,
79
+ dimension: m.dimension ?? null,
80
+ metric: m.metric ?? 'cosine',
81
+ vectorType: m.vectorType ?? 'dense',
82
+ spec: m.spec ?? { serverless: { cloud: 'aws', region: 'us-east-1' } },
83
+ deletionProtection: m.deletionProtection ?? 'disabled',
84
+ tags: m.tags ?? {},
85
+ host: mintHost(m.name),
86
+ ready: true,
87
+ state: 'Ready',
88
+ },
89
+ };
90
+ }
91
+ export function mapVector(v: PineconeRealVector): SyncResource {
92
+ const namespace = v.namespace ?? '';
93
+ return {
94
+ type: 'vector',
95
+ id: vectorKey(v.index, namespace, v.id),
96
+ fields: { values: v.values ?? [], metadata: v.metadata ?? {} },
97
+ };
98
+ }
99
+
100
+ export async function pullPineconeIndexes(rawClient: PineconeLikeClient, opts: { root?: string } & PineconeBudgetedOptions = {}): Promise<SyncResource[]> {
101
+ const client = guardPineconeClient(rawClient, pineconeBudgetOf(opts));
102
+ if (!client.listIndexes) return [];
103
+ return (await client.listIndexes()).indexes.map((m) => mapIndex(m));
104
+ }
105
+ export async function pullPineconeVectors(rawClient: PineconeLikeClient, opts: PineconeBudgetedOptions = {}): Promise<SyncResource[]> {
106
+ const client = guardPineconeClient(rawClient, opts);
107
+ if (!client.listVectors) return [];
108
+ return (await client.listVectors()).vectors.map(mapVector);
109
+ }
110
+
111
+ /**
112
+ * D7 entry point: pull from real Pinecone (indexes + vectors) and fold EVERY domain into the twin
113
+ * via ONE syncPull (shadow-diff dedup). Returns { observed, deltasAppended } — a re-pull of
114
+ * identical state appends ZERO deltas (observed stays, deltasAppended drops to 0).
115
+ */
116
+ export async function syncPineconeFromReal(
117
+ rawClient: PineconeLikeClient,
118
+ opts: { root?: string; occurredAt?: string } & PineconeBudgetedOptions = {},
119
+ ): Promise<{ observed: number; deltasAppended: number }> {
120
+ // Guard ONCE here and hand the guarded client down: this is the entrypoint whose vector
121
+ // aggregation fans out, so it is the one that must be unable to run unbudgeted.
122
+ const client = guardPineconeClient(rawClient, pineconeBudgetOf(opts));
123
+ const occurredAt = opts.occurredAt ?? new Date().toISOString();
124
+ const resources: SyncResource[] = [
125
+ ...(await pullPineconeIndexes(client, opts)),
126
+ ...(await pullPineconeVectors(client)),
127
+ ];
128
+ const result = syncPull({ service: SERVICE, resources, occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
129
+ return { observed: result.observed, deltasAppended: result.deltasAppended };
130
+ }
@@ -0,0 +1,84 @@
1
+ // Pinecone METADATA-FILTER evaluator — net-new, ported CONCEPTUALLY (not copied) from
2
+ // aws-dynamodb-expr.ts's pattern: a genuine interpreter that evaluates real operators against
3
+ // real stored data, not a name/keyword match. The two engines differ in shape because the input
4
+ // differs — DynamoDB's ConditionExpression is a STRING that must be tokenized/parsed, while a
5
+ // Pinecone filter arrives as already-parsed JSON (Mongo-style query syntax) — so this file is a
6
+ // direct recursive evaluator over that JSON, with no tokenizer/parser stage. What is genuinely
7
+ // carried over from aws-dynamodb-expr.ts is the DISCIPLINE: real operator semantics (not string
8
+ // containment), used to gate query/delete results for real, with a crafted query.filter_* test
9
+ // suite proving a filter-ignoring implementation fails (see pinecone-capabilities.ts).
10
+ //
11
+ // Modeled operator subset (Pinecone-documented — see spec-sources.json): field-level
12
+ // `$eq $ne $gt $gte $lt $lte $in $nin`, plus a bare `{field: literal}` shorthand for `$eq`
13
+ // (Pinecone's own documented shorthand), and top-level combinators `$and` / `$or` (arrays of
14
+ // sub-filters). NOT modeled (todo, honest v1 cut): `$exists`, deep dot-path/nested-object
15
+ // metadata addressing, and `null`-value comparison edge semantics — see
16
+ // `pinecone.filters.exists` / `pinecone.filters.deep_nested` / `pinecone.filters.null_semantics`.
17
+ export type PineconeFilter = Record<string, unknown>;
18
+
19
+ const COMPARISON_OPERATORS = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']);
20
+
21
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
22
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
23
+ }
24
+
25
+ function valuesEqual(a: unknown, b: unknown): boolean {
26
+ if (Array.isArray(a) && Array.isArray(b)) {
27
+ return a.length === b.length && a.every((x, i) => valuesEqual(x, b[i]));
28
+ }
29
+ return a === b;
30
+ }
31
+
32
+ function evalComparison(op: string, fieldValue: unknown, operand: unknown): boolean {
33
+ switch (op) {
34
+ case '$eq':
35
+ return valuesEqual(fieldValue, operand);
36
+ case '$ne':
37
+ return !valuesEqual(fieldValue, operand);
38
+ case '$in':
39
+ return Array.isArray(operand) && operand.some((o) => valuesEqual(fieldValue, o));
40
+ case '$nin':
41
+ return Array.isArray(operand) && !operand.some((o) => valuesEqual(fieldValue, o));
42
+ case '$gt':
43
+ case '$gte':
44
+ case '$lt':
45
+ case '$lte': {
46
+ if (typeof fieldValue !== 'number' || typeof operand !== 'number') return false;
47
+ if (op === '$gt') return fieldValue > operand;
48
+ if (op === '$gte') return fieldValue >= operand;
49
+ if (op === '$lt') return fieldValue < operand;
50
+ return fieldValue <= operand;
51
+ }
52
+ default:
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /** Evaluate a single field's clause: either an operator object `{$gt: 5}` or a bare `$eq` literal. */
58
+ function evalFieldClause(fieldValue: unknown, clause: unknown): boolean {
59
+ if (isPlainObject(clause)) {
60
+ const keys = Object.keys(clause);
61
+ if (keys.length > 0 && keys.every((k) => COMPARISON_OPERATORS.has(k))) {
62
+ return keys.every((op) => evalComparison(op, fieldValue, clause[op]));
63
+ }
64
+ }
65
+ return valuesEqual(fieldValue, clause); // bare-literal shorthand for $eq
66
+ }
67
+
68
+ /** Evaluate a Pinecone metadata filter (Mongo-style) against a stored metadata object. An absent/empty filter matches everything. */
69
+ export function matchesFilter(metadata: Record<string, unknown> | undefined, filter: PineconeFilter | undefined | null): boolean {
70
+ if (!filter || Object.keys(filter).length === 0) return true;
71
+ const md = metadata ?? {};
72
+ for (const [key, clause] of Object.entries(filter)) {
73
+ if (key === '$and') {
74
+ if (!Array.isArray(clause) || !clause.every((c) => matchesFilter(md, c as PineconeFilter))) return false;
75
+ continue;
76
+ }
77
+ if (key === '$or') {
78
+ if (!Array.isArray(clause) || clause.length === 0 || !clause.some((c) => matchesFilter(md, c as PineconeFilter))) return false;
79
+ continue;
80
+ }
81
+ if (!evalFieldClause(md[key], clause)) return false;
82
+ }
83
+ return true;
84
+ }
@@ -0,0 +1,75 @@
1
+ // Pinecone twin HTTP server — ONE server hosting BOTH the control plane (api.pinecone.io) and
2
+ // every index's data plane (a per-index minted host, see pinecone-twin.ts's `mintHost`), so an
3
+ // unmodified `@pinecone-database/pinecone` SDK works end-to-end against a single local process.
4
+ //
5
+ // THE ROUTING TRICK (verified against the installed 8.0.0 package's own source — pinecone-sdk.
6
+ // integration.test.ts's header has the full trail): `PineconeConfiguration.fetchApi` is a
7
+ // genuine full-fetch override — BOTH `indexOperationsBuilder` (control plane) and
8
+ // `VectorOperationsProvider`/etc (data plane) read `getFetch(config)`, which prefers
9
+ // `config.fetchApi` over the ambient global `fetch` — so ONE override at `new Pinecone({
10
+ // fetchApi })` covers every request the SDK makes, control AND data plane alike. The twin's own
11
+ // `mintHost()` output is a synthetic, non-resolvable hostname; a caller (real SDK or otherwise)
12
+ // that supplies a `fetchApi` rewriting the URL's host to this server's real `127.0.0.1:<port>`
13
+ // while stashing the ORIGINAL host in the `x-pinecone-target-host` header (exactly what
14
+ // pinecone-sdk.integration.test.ts's `fetchApi` does) reaches this ONE process for every call —
15
+ // the real network is never touched, keeping every capability verify offline + deterministic.
16
+ // Without that header (a plain caller hitting this server directly), the server falls back to
17
+ // the literal HTTP `Host` header, so a caller who already points requests straight at THIS
18
+ // server's address (e.g. `controllerHostUrl: 'http://127.0.0.1:<port>'` for the control plane,
19
+ // `pc.index({ name, host: 'http://127.0.0.1:<port>' })` for the data plane — both real,
20
+ // documented SDK config fields) also works without any header trick at all; the twin's own
21
+ // `pinecone-twin.test.ts` HTTP-server tests drive it exactly that way.
22
+ //
23
+ // Writable by default; pass readOnly to reject writes (per-operation — see pinecone-twin.ts).
24
+ //
25
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createPineconeTwinFetch` and the
26
+ // SERVER is one line of `Bun.serve` around it. This is a CUSTOM fetch, not the kernel adapter
27
+ // (`createTwinFetchFromHandler`): the 202/204 replies are body-suppressed by STATUS whatever the
28
+ // handler returned, which the adapter's body-shaped rule would change.
29
+ import { serveHttp } from '@volter/world-core';
30
+ import { handlePineconeTwinRequest } from './pinecone-twin.ts';
31
+ import { worldNow, statefulTwinManifest} from '@volter/world-core';
32
+
33
+ /** Options every Pinecone-twin HTTP surface needs, independent of who owns the socket. */
34
+ export interface PineconeTwinFetchOptions {
35
+ root?: string;
36
+ readOnly?: boolean;
37
+ }
38
+
39
+ export function createPineconeTwinFetch(options: PineconeTwinFetchOptions): (request: Request) => Promise<Response> {
40
+ const readOnly = options.readOnly ?? false;
41
+ return async function pineconeTwinFetch(request: Request): Promise<Response> {
42
+ const url = new URL(request.url);
43
+ // GET /twin — the discovery manifest (education inside the twin).
44
+ if (request.method === 'GET' && url.pathname.replace(/\/+$/, '') === '/twin') {
45
+ return Response.json(statefulTwinManifest({ vendor: 'pinecone', twinOf: 'the Pinecone vector database', stores: 'indexes, collections and vectors, searched by a real brute-force scorer' }));
46
+ }
47
+ const headers: Record<string, string> = {};
48
+ request.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
49
+ const host = headers['x-pinecone-target-host'] || headers['host'] || url.host;
50
+ const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : '';
51
+ const { status, body: out, headers: outHeaders } = await handlePineconeTwinRequest({
52
+ method: request.method,
53
+ path: url.pathname + (url.search || ''),
54
+ body,
55
+ headers,
56
+ host,
57
+ readOnly,
58
+ occurredAt: worldNow(),
59
+ ...(options.root !== undefined ? { root: options.root } : {}),
60
+ });
61
+ if (status === 202 || status === 204 || out === null) {
62
+ return new Response(null, { status, headers: { ...(outHeaders ?? {}) } });
63
+ }
64
+ return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', ...(outHeaders ?? {}) } });
65
+ };
66
+ }
67
+
68
+ export async function createPineconeTwinServer(options: { root?: string; port?: number; readOnly?: boolean } = {}): Promise<{ port: number; stop: () => void }> {
69
+ const server = await serveHttp({
70
+ port: options.port ?? 0,
71
+ idleTimeout: 60,
72
+ fetch: createPineconeTwinFetch(options),
73
+ });
74
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
75
+ }
@@ -0,0 +1,71 @@
1
+ // Pinecone SIMILARITY MATH — net-new, no clone (ADDING_A_TWIN.md's "real computed op as strong
2
+ // done" pattern; the analog of aws-dynamodb's Query returning computed results offline). This is
3
+ // REAL brute-force top-K search over the stored vector values, not a stub: exact cosine /
4
+ // dotproduct / euclidean, computed and sorted for real. Exact search is a faithful SUPERSET of
5
+ // Pinecone's real approximate nearest-neighbor (ANN) index — the matches/scores this twin
6
+ // returns are CORRECT, just not approximated the way a real HNSW/IVF index would be.
7
+ //
8
+ // Metric semantics (grounded facts + one doc-UNVERIFIED ⚠, recorded in spec-sources.json):
9
+ // cosine — cosine similarity in [-1, 1]; HIGHER score = more similar (descending sort).
10
+ // dotproduct — raw dot product; HIGHER score = more similar (descending sort).
11
+ // euclidean ⚠ — this twin returns the SQUARED L2 distance (sum of squared per-dimension
12
+ // differences — the conventional Pinecone computation, avoiding an unnecessary sqrt: squaring
13
+ // is monotonic so it never changes the ranking). LOWER score = more similar (ascending sort).
14
+ // The public Pinecone docs fetched during this build did not state the exact wire-level score
15
+ // value or sort direction for euclidean (WebFetch against docs.pinecone.io/guides/index-data/
16
+ // indexing-overview came back with no coverage of distance-metric scoring) — this is doc-
17
+ // UNVERIFIED. The ascending-is-closer DIRECTION is not a guessed API convention, though: it is
18
+ // the definitional meaning of "distance" (0 = identical vectors = most similar), so it is
19
+ // modeled as `done` rather than demoted to `todo`; only the exact SIGN/SCALE of the number
20
+ // Pinecone's real API returns is unverified.
21
+ export type PineconeMetric = 'cosine' | 'dotproduct' | 'euclidean';
22
+
23
+ export function dotProduct(a: readonly number[], b: readonly number[]): number {
24
+ const n = Math.min(a.length, b.length);
25
+ let sum = 0;
26
+ for (let i = 0; i < n; i++) sum += a[i]! * b[i]!;
27
+ return sum;
28
+ }
29
+
30
+ export function magnitude(v: readonly number[]): number {
31
+ let sum = 0;
32
+ for (const x of v) sum += x * x;
33
+ return Math.sqrt(sum);
34
+ }
35
+
36
+ /** Cosine similarity in [-1, 1]; 0 if either vector has zero magnitude (undefined direction). */
37
+ export function cosineSimilarity(a: readonly number[], b: readonly number[]): number {
38
+ const magA = magnitude(a);
39
+ const magB = magnitude(b);
40
+ if (magA === 0 || magB === 0) return 0;
41
+ return dotProduct(a, b) / (magA * magB);
42
+ }
43
+
44
+ /** Squared L2 distance (sum of squared differences). Mismatched lengths pad the shorter with 0. */
45
+ export function squaredEuclideanDistance(a: readonly number[], b: readonly number[]): number {
46
+ const n = Math.max(a.length, b.length);
47
+ let sum = 0;
48
+ for (let i = 0; i < n; i++) {
49
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
50
+ sum += d * d;
51
+ }
52
+ return sum;
53
+ }
54
+
55
+ /** Score one candidate against a query vector for the given metric. */
56
+ export function scoreVector(metric: PineconeMetric, query: readonly number[], candidate: readonly number[]): number {
57
+ if (metric === 'cosine') return cosineSimilarity(query, candidate);
58
+ if (metric === 'dotproduct') return dotProduct(query, candidate);
59
+ return squaredEuclideanDistance(query, candidate);
60
+ }
61
+
62
+ /** True when a HIGHER score means more similar (false only for euclidean: lower/ascending). */
63
+ export function higherIsBetter(metric: PineconeMetric): boolean {
64
+ return metric !== 'euclidean';
65
+ }
66
+
67
+ /** Sort scored candidates best-match-first for the given metric (stable). */
68
+ export function sortByScore<T extends { score: number }>(metric: PineconeMetric, scored: readonly T[]): T[] {
69
+ const ascending = !higherIsBetter(metric);
70
+ return [...scored].sort((a, b) => (ascending ? a.score - b.score : b.score - a.score));
71
+ }