okengine 0.3.5 → 0.4.3

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.
Files changed (144) hide show
  1. package/AGENTS.md +6 -0
  2. package/manifest.v1.schema.json +43 -1
  3. package/package.json +27 -12
  4. package/site/content/docs/ai/skills.mdx +11 -7
  5. package/site/content/docs/console/vault.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +2 -0
  7. package/site/content/docs/elements/channel.mdx +7 -4
  8. package/site/content/docs/elements/clock.mdx +2 -4
  9. package/site/content/docs/elements/flow.mdx +19 -10
  10. package/site/content/docs/elements/gate.mdx +18 -11
  11. package/site/content/docs/elements/signal.mdx +9 -12
  12. package/site/content/docs/elements/store.mdx +69 -8
  13. package/site/content/docs/elements/vault.mdx +11 -12
  14. package/site/content/docs/get-started/basic-usage.mdx +76 -41
  15. package/site/content/docs/get-started/installation.mdx +95 -43
  16. package/site/content/docs/get-started/introduction.mdx +128 -75
  17. package/site/content/docs/get-started/meta.json +1 -1
  18. package/site/content/docs/get-started/why.mdx +141 -0
  19. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  20. package/site/content/docs/plugins/security-headers.mdx +1 -1
  21. package/site/content/docs/reference/configuration.mdx +2 -2
  22. package/site/content/docs/reference/environment-variables.mdx +10 -0
  23. package/site/content/docs/reference/fx.mdx +56 -3
  24. package/site/content/docs/reference/plugins.mdx +18 -18
  25. package/src/cli/competitor-mention-removal.test.ts +117 -0
  26. package/src/cli/dev.ts +20 -0
  27. package/src/cli/meilisearch-local.test.ts +69 -0
  28. package/src/cli/meilisearch-local.ts +188 -0
  29. package/src/compiler/extract.test.ts +63 -0
  30. package/src/compiler/extract.ts +36 -15
  31. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  32. package/src/console/server/channels.ts +2 -0
  33. package/src/console/server/clock.ts +3 -0
  34. package/src/console/server/flows.ts +13 -0
  35. package/src/console/server/gates.ts +2 -0
  36. package/src/console/server/plugins.ts +20 -1
  37. package/src/console/server/signals.ts +5 -0
  38. package/src/console/server/store.test.ts +17 -0
  39. package/src/console/server/store.ts +35 -1
  40. package/src/console/ui/channels/types.ts +1 -0
  41. package/src/console/ui/clock/types.ts +1 -0
  42. package/src/console/ui/display.test.ts +14 -0
  43. package/src/console/ui/display.ts +9 -0
  44. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  45. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  46. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  47. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  48. package/src/console/ui/dist/assets/panel-channels-9T3ybqRu.js +1 -0
  49. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  50. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  51. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  52. package/src/console/ui/dist/assets/panel-gates-BQGYXvjT.js +1 -0
  53. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  54. package/src/console/ui/dist/assets/panel-plugins-D0PsmVw2.js +1 -0
  55. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  56. package/src/console/ui/dist/assets/panel-signals-Bbg4ewpP.js +1 -0
  57. package/src/console/ui/dist/assets/panel-store-CPCbsDRa.js +1 -0
  58. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  59. package/src/console/ui/dist/assets/panel-vault-D1_MvOmo.js +1 -0
  60. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  61. package/src/console/ui/dist/index.html +2 -2
  62. package/src/console/ui/gates/types.ts +1 -0
  63. package/src/console/ui/plugins/fixture.ts +7 -0
  64. package/src/console/ui/plugins/types.ts +3 -0
  65. package/src/console/ui/shell/client.ts +3 -0
  66. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
  67. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
  68. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
  69. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
  70. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
  71. package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
  72. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
  73. package/src/console/ui/signals/types.ts +1 -0
  74. package/src/console/ui/store/fixture.ts +5 -0
  75. package/src/console/ui/store/types.ts +2 -0
  76. package/src/docker/compose.ts +5 -0
  77. package/src/docker/docker.test.ts +41 -0
  78. package/src/docker/recipes/index.ts +10 -2
  79. package/src/docker/recipes/meilisearch.ts +31 -0
  80. package/src/drivers/conformance.test.ts +26 -0
  81. package/src/drivers/conformance.ts +40 -3
  82. package/src/drivers/drizzle-dialect.test.ts +4 -0
  83. package/src/drivers/drizzle-dialect.ts +8 -4
  84. package/src/drivers/index.ts +18 -2
  85. package/src/drivers/libsql.ts +179 -0
  86. package/src/drivers/meilisearch.integration.test.ts +77 -0
  87. package/src/drivers/meilisearch.test.ts +181 -0
  88. package/src/drivers/meilisearch.ts +208 -0
  89. package/src/drivers/memory.ts +4 -4
  90. package/src/drivers/pglite.ts +79 -0
  91. package/src/drivers/pgvector.ts +60 -25
  92. package/src/drivers/types.ts +106 -16
  93. package/src/drivers/vault-driver-removal.test.ts +6 -0
  94. package/src/drivers/vault-types.ts +4 -4
  95. package/src/elements/ai/runtime.ts +6 -0
  96. package/src/elements/ai.test.ts +22 -0
  97. package/src/elements/channel/declare.ts +5 -0
  98. package/src/elements/clock/declare.ts +5 -0
  99. package/src/elements/clock/durable.ts +7 -1
  100. package/src/elements/gate/declare.ts +28 -5
  101. package/src/elements/gate.ts +1 -0
  102. package/src/elements/signal/declare.ts +5 -0
  103. package/src/elements/store/declare.ts +10 -2
  104. package/src/elements/store/index-boot.test.ts +299 -0
  105. package/src/elements/store/runtime.ts +110 -17
  106. package/src/elements/store/schema-decl.ts +7 -0
  107. package/src/elements/store.ts +2 -0
  108. package/src/elements/vault.test.ts +27 -4
  109. package/src/elements/vault.ts +1 -1
  110. package/src/index.ts +4 -0
  111. package/src/kernel/abort-scope.ts +116 -0
  112. package/src/kernel/app.ts +12 -2
  113. package/src/kernel/boot-bind/store.test.ts +68 -1
  114. package/src/kernel/boot-bind/store.ts +92 -2
  115. package/src/kernel/concurrency.test.ts +294 -0
  116. package/src/kernel/concurrency.ts +220 -0
  117. package/src/kernel/flow.ts +9 -0
  118. package/src/kernel/fx.test.ts +23 -2
  119. package/src/kernel/fx.ts +80 -4
  120. package/src/kernel/index.ts +23 -0
  121. package/src/kernel/journal.ts +9 -0
  122. package/src/kernel/plugin/capabilities.test.ts +18 -0
  123. package/src/kernel/plugin.ts +11 -3
  124. package/src/kernel/redacted.ts +74 -0
  125. package/src/kernel/registry.ts +29 -6
  126. package/src/kernel/router.ts +3 -3
  127. package/src/manifest/types.ts +21 -0
  128. package/src/release/measure.ts +4 -0
  129. package/src/test/provisions.integration.test.ts +1 -1
  130. package/site/content/docs/get-started/comparison.mdx +0 -65
  131. package/src/console/ui/dist/assets/index-BWo8R7NR.js +0 -10
  132. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  133. package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
  134. package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
  135. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  136. package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
  137. package/src/console/ui/dist/assets/panel-overview-BznEOTnb.js +0 -1
  138. package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
  139. package/src/console/ui/dist/assets/panel-runs-CGWNHLR4.js +0 -1
  140. package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
  141. package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
  142. package/src/console/ui/dist/assets/panel-traces-DBLx2ilD.js +0 -1
  143. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
  144. package/src/drivers/vault-infisical.ts +0 -57
@@ -17,6 +17,8 @@ export interface SignalOptions {
17
17
  * - `live` — client-subscribable stream (replayable)
18
18
  */
19
19
  readonly delivery: SignalDelivery;
20
+ /** Optional human description for Console / docs (falls back to the signal name). */
21
+ readonly description?: string;
20
22
  /** Max delivery attempts before dead-letter (once). */
21
23
  readonly retries?: number;
22
24
  /** Preserve exhausted messages in the DLQ (once). */
@@ -35,6 +37,8 @@ export interface SignalDecl<T = unknown> {
35
37
  readonly name: string;
36
38
  /** Delivery physics. */
37
39
  readonly delivery: SignalDelivery;
40
+ /** Optional human description. */
41
+ readonly description?: string;
38
42
  /** Max retries before DLQ. */
39
43
  readonly retries: number;
40
44
  /** Whether exhausted messages enter the DLQ. */
@@ -64,6 +68,7 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
64
68
  return {
65
69
  name,
66
70
  delivery: options.delivery,
71
+ ...(options.description !== undefined ? { description: options.description } : {}),
67
72
  retries: options.retries ?? 3,
68
73
  deadLetter: options.deadLetter ?? true,
69
74
  schema: options.schema,
@@ -40,6 +40,8 @@ export interface StoreDeclBase {
40
40
 
41
41
  /** Options for {@link store.sql}. */
42
42
  export interface SqlStoreOptions {
43
+ /** Optional human description for Console / docs (falls back to the store name). */
44
+ readonly description?: string;
43
45
  /**
44
46
  * Schema tables — okengine {@link TableHandle}s and/or Drizzle tables.
45
47
  * Classification is read from TableHandle columns when present.
@@ -56,6 +58,7 @@ export interface SqlStoreOptions {
56
58
  export interface SqlStoreDecl extends StoreDeclBase {
57
59
  readonly facet: "sql";
58
60
  readonly ref: `sql:${string}`;
61
+ readonly description?: string;
59
62
  readonly schema?: SqlStoreOptions["schema"];
60
63
  readonly classify?: SqlStoreOptions["classify"];
61
64
  /**
@@ -94,6 +97,8 @@ export interface FilesStoreDecl extends StoreDeclBase {
94
97
 
95
98
  /** Options for {@link store.index}. */
96
99
  export interface IndexStoreOptions {
100
+ /** Optional human description for Console / docs (falls back to the index name). */
101
+ readonly description?: string;
97
102
  /** Vector dimensions (default 3 for tests; set explicitly in apps). */
98
103
  readonly dims?: number;
99
104
  }
@@ -102,6 +107,7 @@ export interface IndexStoreOptions {
102
107
  export interface IndexStoreDecl extends StoreDeclBase {
103
108
  readonly facet: "index";
104
109
  readonly ref: `index:${string}`;
110
+ readonly description?: string;
105
111
  readonly dims?: number;
106
112
  }
107
113
 
@@ -119,6 +125,7 @@ export function sql(name: string, options: SqlStoreOptions = {}): SqlStoreDecl {
119
125
  facet: "sql",
120
126
  name,
121
127
  ref: `sql:${name}`,
128
+ ...(options.description !== undefined ? { description: options.description } : {}),
122
129
  schema: options.schema,
123
130
  classify: options.classify,
124
131
  table(table) {
@@ -147,7 +154,7 @@ export function kv(name: string, options: KvStoreOptions = {}): KvStoreDecl {
147
154
  facet: "kv",
148
155
  name,
149
156
  ref: `kv:${name}`,
150
- description: options.description,
157
+ ...(options.description !== undefined ? { description: options.description } : {}),
151
158
  };
152
159
  }
153
160
 
@@ -162,7 +169,7 @@ export function files(name: string, options: FilesStoreOptions = {}): FilesStore
162
169
  facet: "files",
163
170
  name,
164
171
  ref: `files:${name}`,
165
- description: options.description,
172
+ ...(options.description !== undefined ? { description: options.description } : {}),
166
173
  };
167
174
  }
168
175
 
@@ -177,6 +184,7 @@ export function index(name: string, options: IndexStoreOptions = {}): IndexStore
177
184
  facet: "index",
178
185
  name,
179
186
  ref: `index:${name}`,
187
+ ...(options.description !== undefined ? { description: options.description } : {}),
180
188
  dims: options.dims,
181
189
  };
182
190
  }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * End-to-end `store.index()` boot wiring — the CONFIGURED driver resolves at
3
+ * real boot for every backend (memory / libsql / pgvector over pglite /
4
+ * pgvector over real Postgres), SQL-backed indexes share store.sql's
5
+ * already-open connection (never a second, redundant one), and search runs
6
+ * through each engine's native ANN index — no JS-side full scan survives.
7
+ */
8
+
9
+ import { afterEach, describe, expect, test } from "bun:test";
10
+ import {
11
+ connectLibsql,
12
+ libsqlDriver,
13
+ libsqlIndexDriver,
14
+ openLibsqlIndex,
15
+ } from "../../drivers/libsql.ts";
16
+ import { connectPglite } from "../../drivers/pglite.ts";
17
+ import { openPgvectorIndex } from "../../drivers/pgvector.ts";
18
+ import { connectPostgres } from "../../drivers/postgres.ts";
19
+ import type { SqlConnection, SqlDriver } from "../../drivers/types.ts";
20
+ import { bindStore } from "../../kernel/boot-bind/store.ts";
21
+ import {
22
+ createStoreRuntime,
23
+ index as declareIndex,
24
+ sql as declareSql,
25
+ type SqlStoreHandle,
26
+ type VectorIndexStoreFxHandle,
27
+ } from "../store.ts";
28
+
29
+ const WRITE_CTX = (name: string) => ({
30
+ effects: { reads: [`index:${name}` as const], writes: [`index:${name}` as const] },
31
+ });
32
+
33
+ const prev = {
34
+ libsql: process.env.OKE_LIBSQL_URL,
35
+ pglite: process.env.OKE_PGLITE_URL,
36
+ };
37
+
38
+ afterEach(() => {
39
+ if (prev.libsql === undefined) delete process.env.OKE_LIBSQL_URL;
40
+ else process.env.OKE_LIBSQL_URL = prev.libsql;
41
+ if (prev.pglite === undefined) delete process.env.OKE_PGLITE_URL;
42
+ else process.env.OKE_PGLITE_URL = prev.pglite;
43
+ });
44
+
45
+ /** Wrap a real connection so the SQL it executes can be asserted on. */
46
+ function recordingConn(conn: SqlConnection): { conn: SqlConnection; statements: string[] } {
47
+ const statements: string[] = [];
48
+ return {
49
+ statements,
50
+ conn: {
51
+ driverId: conn.driverId,
52
+ role: conn.role,
53
+ query: (sql, params) => {
54
+ statements.push(sql);
55
+ return conn.query(sql, params);
56
+ },
57
+ exec: (sql, params) => {
58
+ statements.push(sql);
59
+ return conn.exec(sql, params);
60
+ },
61
+ close: () => conn.close(),
62
+ },
63
+ };
64
+ }
65
+
66
+ describe("store.index boot wiring — memory", () => {
67
+ test("boot with no configured index driver resolves memory end to end", async () => {
68
+ const kb = declareIndex("kb", { dims: 3 });
69
+ const runtime = bindStore({ stores: [kb] }, "local", () => Date.now());
70
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
71
+ expect(handle.driverId).toBe("memory");
72
+ await handle.upsert("near", [1, 0, 0]);
73
+ await handle.upsert("far", [0, 1, 0]);
74
+ const hits = await handle.search([1, 0, 0], 2);
75
+ expect(hits[0]?.id).toBe("near");
76
+ await runtime.close();
77
+ });
78
+ });
79
+
80
+ describe("store.index boot wiring — libsql", () => {
81
+ test("boot: sql=libsql + index=libsql resolves native ANN end to end", async () => {
82
+ process.env.OKE_LIBSQL_URL = ":memory:";
83
+ const kb = declareIndex("kb", { dims: 3 });
84
+ const runtime = bindStore(
85
+ {
86
+ config: { drivers: { store: { sql: { local: "libsql" }, index: { local: "libsql" } } } },
87
+ stores: [kb],
88
+ },
89
+ "local",
90
+ () => Date.now(),
91
+ );
92
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
93
+ expect(handle.driverId).toBe("libsql");
94
+
95
+ await handle.upsert("near", [1, 0.1, 0], { label: "near" });
96
+ await handle.upsert("far", [0, 1, 0]);
97
+ await handle.upsert("farther", [-1, 0, 0]);
98
+ const hits = await handle.search([1, 0, 0], 3);
99
+ expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
100
+ expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
101
+ expect(hits[0]!.meta).toEqual({ label: "near" });
102
+ expect(await handle.delete("farther")).toBe(true);
103
+ await runtime.close();
104
+ });
105
+
106
+ test("sql and index facets share one already-open libsql connection", async () => {
107
+ let connects = 0;
108
+ const countingLibsql: SqlDriver = {
109
+ ...libsqlDriver,
110
+ connect: async (opts) => {
111
+ connects++;
112
+ return libsqlDriver.connect(opts);
113
+ },
114
+ };
115
+ const docs = declareSql("docs");
116
+ const kb = declareIndex("kb", { dims: 3 });
117
+ const runtime = createStoreRuntime({
118
+ drivers: { sql: countingLibsql, index: libsqlIndexDriver },
119
+ sql: { docs: { name: "docs", primary: { url: ":memory:" } } },
120
+ index: { kb: { url: ":memory:" } },
121
+ });
122
+ // Index first: it must open the connection the sql facet then reuses.
123
+ const idxHandle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
124
+ await idxHandle.upsert("a", [1, 0, 0]);
125
+ const sqlHandle = (await runtime.open(docs, {
126
+ effects: { reads: ["sql:docs"], writes: ["sql:docs"] },
127
+ })) as SqlStoreHandle;
128
+ await sqlHandle.raw("CREATE TABLE IF NOT EXISTS t_share (id TEXT)", []);
129
+ expect(connects).toBe(1);
130
+ await runtime.close();
131
+ });
132
+
133
+ test("libsql index searches through vector_top_k — real ANN, not a scan", async () => {
134
+ const real = await connectLibsql({ url: ":memory:" });
135
+ const { conn, statements } = recordingConn(real);
136
+ const idx = await openLibsqlIndex({ name: "ann", dims: 3, sql: conn });
137
+ await idx.upsert("x", [1, 0, 0]);
138
+ await idx.upsert("y", [0, 1, 0]);
139
+ const hits = await idx.search([1, 0, 0], 1);
140
+ expect(hits[0]?.id).toBe("x");
141
+ expect(statements.some((s) => s.includes("libsql_vector_idx"))).toBe(true);
142
+ expect(statements.some((s) => s.includes("vector_top_k"))).toBe(true);
143
+ const plan = await real.query(
144
+ `EXPLAIN QUERY PLAN SELECT * FROM vector_top_k('oke_idx_ann_vec', vector32('[1,0,0]'), 1)`,
145
+ );
146
+ expect(JSON.stringify(plan)).toContain("vector_top_k");
147
+ await idx.close();
148
+ await real.close();
149
+ });
150
+ });
151
+
152
+ describe("store.index boot wiring — pglite + pgvector", () => {
153
+ test("boot: sql=pglite + index=pgvector resolves end to end", async () => {
154
+ process.env.OKE_PGLITE_URL = "memory://";
155
+ const kb = declareIndex("kb", { dims: 3 });
156
+ const runtime = bindStore(
157
+ {
158
+ config: { drivers: { store: { sql: { local: "pglite" }, index: { local: "pgvector" } } } },
159
+ stores: [kb],
160
+ },
161
+ "local",
162
+ () => Date.now(),
163
+ );
164
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
165
+ expect(handle.driverId).toBe("pgvector");
166
+
167
+ await handle.upsert("near", [1, 0.1, 0], { label: "near" });
168
+ await handle.upsert("far", [0, 1, 0]);
169
+ await handle.upsert("farther", [-1, 0, 0]);
170
+ const hits = await handle.search([1, 0, 0], 3);
171
+ expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
172
+ expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
173
+ expect(hits[0]!.meta).toEqual({ label: "near" });
174
+ expect(await handle.delete("farther")).toBe(true);
175
+ await runtime.close();
176
+ });
177
+
178
+ test("pglite runs the shared pgvector HNSW path — real ANN, not a scan", async () => {
179
+ const real = await connectPglite({ url: "memory://" });
180
+ const { conn, statements } = recordingConn(real);
181
+ const idx = await openPgvectorIndex({ name: "ann", dims: 3, sql: conn });
182
+ await idx.upsert("a", [1, 0, 0]);
183
+ await idx.upsert("b", [0, 1, 0]);
184
+ const hits = await idx.search([1, 0, 0], 2);
185
+ expect(hits[0]?.id).toBe("a");
186
+ expect(statements.some((s) => s.includes("CREATE EXTENSION IF NOT EXISTS vector"))).toBe(true);
187
+ expect(
188
+ statements.some((s) => s.includes("USING hnsw") && s.includes("vector_cosine_ops")),
189
+ ).toBe(true);
190
+ expect(statements.some((s) => s.includes("<=>"))).toBe(true);
191
+ const indexes = await real.query(
192
+ `SELECT indexdef FROM pg_indexes WHERE tablename = 'oke_idx_ann'`,
193
+ );
194
+ expect(JSON.stringify(indexes)).toContain("USING hnsw");
195
+ await idx.close();
196
+ await real.close();
197
+ });
198
+ });
199
+
200
+ describe("store.index boot wiring — fail loud", () => {
201
+ test("pgvector index with memory sql driver throws — never silently memory", async () => {
202
+ const kb = declareIndex("kb", { dims: 3 });
203
+ const runtime = bindStore(
204
+ {
205
+ config: { drivers: { store: { sql: { local: "memory" }, index: { local: "pgvector" } } } },
206
+ stores: [kb],
207
+ },
208
+ "local",
209
+ () => Date.now(),
210
+ );
211
+ await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/cannot share sql driver/);
212
+ await runtime.close();
213
+ });
214
+
215
+ test("libsql index with sqlite sql driver throws", async () => {
216
+ const kb = declareIndex("kb", { dims: 3 });
217
+ const runtime = bindStore(
218
+ {
219
+ config: { drivers: { store: { sql: { local: "sqlite" }, index: { local: "libsql" } } } },
220
+ stores: [kb],
221
+ },
222
+ "local",
223
+ () => Date.now(),
224
+ );
225
+ await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/cannot share sql driver/);
226
+ await runtime.close();
227
+ });
228
+
229
+ test("meilisearch without OKE_STORE_INDEX_URL throws at bind — never sqlUrl, never memory", () => {
230
+ const prev = process.env.OKE_STORE_INDEX_URL;
231
+ delete process.env.OKE_STORE_INDEX_URL;
232
+ try {
233
+ const kb = declareIndex("kb");
234
+ expect(() =>
235
+ bindStore(
236
+ { config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
237
+ "local",
238
+ () => Date.now(),
239
+ ),
240
+ ).toThrow(/OKE_STORE_INDEX_URL/);
241
+ } finally {
242
+ if (prev === undefined) delete process.env.OKE_STORE_INDEX_URL;
243
+ else process.env.OKE_STORE_INDEX_URL = prev;
244
+ }
245
+ });
246
+
247
+ test("meilisearch binds its own URL + key (never the sql connection)", async () => {
248
+ const prevUrl = process.env.OKE_STORE_INDEX_URL;
249
+ const prevKey = process.env.OKE_STORE_INDEX_KEY;
250
+ process.env.OKE_STORE_INDEX_URL = "http://127.0.0.1:7700";
251
+ process.env.OKE_STORE_INDEX_KEY = "master-key";
252
+ try {
253
+ const kb = declareIndex("kb");
254
+ const runtime = bindStore(
255
+ { config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
256
+ "local",
257
+ () => Date.now(),
258
+ );
259
+ // No live server in unit tests: opening must fail loud (unreachable),
260
+ // proving the binding pointed at HTTP, not silently degraded to memory.
261
+ await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/meilisearch index/);
262
+ await runtime.close();
263
+ } finally {
264
+ if (prevUrl === undefined) delete process.env.OKE_STORE_INDEX_URL;
265
+ else process.env.OKE_STORE_INDEX_URL = prevUrl;
266
+ if (prevKey === undefined) delete process.env.OKE_STORE_INDEX_KEY;
267
+ else process.env.OKE_STORE_INDEX_KEY = prevKey;
268
+ }
269
+ });
270
+ });
271
+
272
+ const PGVECTOR_URL = process.env.OKE_TEST_PGVECTOR_URL;
273
+ if (!PGVECTOR_URL) {
274
+ console.log("skip: live pgvector e2e (OKE_TEST_PGVECTOR_URL not set)");
275
+ }
276
+ const liveTest = PGVECTOR_URL ? test : test.skip;
277
+
278
+ describe("store.index boot wiring — real Postgres pgvector", () => {
279
+ liveTest("live Postgres runs real HNSW ANN, not a scan", async () => {
280
+ const real = await connectPostgres({ url: PGVECTOR_URL });
281
+ const { conn, statements } = recordingConn(real);
282
+ const idx = await openPgvectorIndex({ name: "live", dims: 3, sql: conn });
283
+ await idx.upsert("near", [1, 0.1, 0]);
284
+ await idx.upsert("far", [0, 1, 0]);
285
+ await idx.upsert("farther", [-1, 0, 0]);
286
+ const hits = await idx.search([1, 0, 0], 3);
287
+ expect(hits.map((h) => h.id)).toEqual(["near", "far", "farther"]);
288
+ expect(statements.some((s) => s.includes("CREATE EXTENSION IF NOT EXISTS vector"))).toBe(true);
289
+ expect(statements.some((s) => s.includes("USING hnsw"))).toBe(true);
290
+ expect(statements.some((s) => s.includes("<=>"))).toBe(true);
291
+ const indexes = await real.query(
292
+ `SELECT indexdef FROM pg_indexes WHERE tablename = 'oke_idx_live'`,
293
+ );
294
+ expect(JSON.stringify(indexes)).toContain("USING hnsw");
295
+ await idx.delete("farther");
296
+ await idx.close();
297
+ await real.close();
298
+ });
299
+ });
@@ -14,9 +14,16 @@ import type {
14
14
  KvDriver,
15
15
  KvNamespace,
16
16
  SqlConnection,
17
+ SqlConnectOptions,
17
18
  SqlDriver,
19
+ SqlRole,
18
20
  SqlRow,
21
+ TextIndexSearchOptions,
22
+ TextIndexSearchResult,
23
+ VectorIndexDriver,
24
+ VectorIndexStore,
19
25
  } from "../../drivers/types.ts";
26
+ import { indexDriverNeedsSql } from "../../drivers/types.ts";
20
27
  import { buildClassificationMap, type MaskRowsOptions } from "./classify.ts";
21
28
  import {
22
29
  createStoreCache,
@@ -62,7 +69,15 @@ export interface CreateStoreRuntimeOptions {
62
69
  readonly files?: Readonly<Record<string, { readonly root?: string; readonly client?: unknown }>>;
63
70
  /** Index open options keyed by store name. */
64
71
  readonly index?: Readonly<
65
- Record<string, { readonly dims?: number; readonly url?: string; readonly sql?: SqlConnection }>
72
+ Record<
73
+ string,
74
+ {
75
+ readonly dims?: number;
76
+ readonly url?: string;
77
+ readonly apiKey?: string;
78
+ readonly sql?: SqlConnection;
79
+ }
80
+ >
66
81
  >;
67
82
  /** Clock for cache TTLs. */
68
83
  readonly now?: () => number;
@@ -113,18 +128,30 @@ export interface FilesStoreFxHandle {
113
128
  list(prefix?: string): Promise<string[]>;
114
129
  }
115
130
 
116
- /** Index handle on `fx.store`. */
117
- export interface IndexStoreFxHandle {
131
+ /** Vector index handle on `fx.store`. */
132
+ export interface VectorIndexStoreFxHandle {
118
133
  readonly ref: `index:${string}`;
119
- readonly driverId: "memory" | "pgvector";
134
+ readonly driverId: "memory" | "pgvector" | "libsql";
120
135
  upsert(id: string, vector: readonly number[], meta?: Record<string, unknown>): Promise<void>;
121
136
  search(
122
137
  vector: readonly number[],
123
138
  topK?: number,
124
- ): Promise<Array<{ id: string; score: number; meta?: Record<string, unknown> }>>;
139
+ ): Promise<ReadonlyArray<{ id: string; score: number; meta?: Record<string, unknown> }>>;
125
140
  delete(id: string): Promise<boolean>;
126
141
  }
127
142
 
143
+ /** Full-text index handle on `fx.store`. */
144
+ export interface TextIndexStoreFxHandle {
145
+ readonly ref: `index:${string}`;
146
+ readonly driverId: "meilisearch";
147
+ upsert(id: string, document: Record<string, unknown>): Promise<void>;
148
+ search(q: string, opts?: TextIndexSearchOptions): Promise<TextIndexSearchResult>;
149
+ delete(id: string): Promise<boolean>;
150
+ }
151
+
152
+ /** Index handle on `fx.store` — discriminated by `driverId`. */
153
+ export type IndexStoreFxHandle = VectorIndexStoreFxHandle | TextIndexStoreFxHandle;
154
+
128
155
  /** Store runtime. */
129
156
  export interface StoreRuntime {
130
157
  /** Shared multi-tier cache. */
@@ -186,6 +213,19 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
186
213
  const kvNs = new Map<string, KvNamespace>();
187
214
  const fileBuckets = new Map<string, FilesBucket>();
188
215
  const indexes = new Map<string, IndexStore>();
216
+ const clientIds = new WeakMap<object, number>();
217
+ let nextClientId = 0;
218
+
219
+ /** Injected test clients with the same URL still get distinct cache entries. */
220
+ function clientTag(client: unknown): string {
221
+ if ((typeof client !== "object" && typeof client !== "function") || client === null) return "";
222
+ let id = clientIds.get(client as object);
223
+ if (id === undefined) {
224
+ id = ++nextClientId;
225
+ clientIds.set(client as object, id);
226
+ }
227
+ return `#c${id}`;
228
+ }
189
229
 
190
230
  function classificationsFor(decl: SqlStoreDecl): ClassificationMap {
191
231
  const nested: Record<
@@ -215,13 +255,7 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
215
255
  const driver = options.drivers.sql;
216
256
  if (!driver) throw new Error("No sql driver configured");
217
257
 
218
- const cacheKey = `${decl.name}:${target.role}:${JSON.stringify(target.options.url ?? "")}`;
219
- let conn = sqlConns.get(cacheKey);
220
- if (!conn) {
221
- conn = await driver.connect(target.options);
222
- sqlConns.set(cacheKey, conn);
223
- }
224
-
258
+ const conn = await sharedSqlConn(driver, target.role, target.options);
225
259
  return createSqlStoreHandle(`sql:${decl.name}`, {
226
260
  connection: conn,
227
261
  classifications: classificationsFor(decl),
@@ -231,6 +265,51 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
231
265
  });
232
266
  }
233
267
 
268
+ /**
269
+ * One connection per driver+role+URL (+ injected client) — `store.sql` and a
270
+ * SQL-backed `store.index` share the same already-open connection, never a
271
+ * second one.
272
+ */
273
+ async function sharedSqlConn(
274
+ driver: SqlDriver,
275
+ role: SqlRole,
276
+ connectOptions: SqlConnectOptions,
277
+ ): Promise<SqlConnection> {
278
+ const cacheKey = `${driver.id}:${role}:${connectOptions.url ?? ""}${clientTag(connectOptions.client)}`;
279
+ let conn = sqlConns.get(cacheKey);
280
+ if (!conn) {
281
+ conn = await driver.connect({ ...connectOptions, role });
282
+ sqlConns.set(cacheKey, conn);
283
+ }
284
+ return conn;
285
+ }
286
+
287
+ /** SQL-backed index drivers borrow the sql facet's connection. */
288
+ async function sqlConnForIndex(
289
+ driver: VectorIndexDriver & { id: "pgvector" | "libsql" },
290
+ url: string | undefined,
291
+ ): Promise<SqlConnection> {
292
+ const sqlDriver = options.drivers.sql;
293
+ if (!sqlDriver) {
294
+ throw new Error(
295
+ `oke store: index driver "${driver.id}" needs a configured sql driver to share its connection`,
296
+ );
297
+ }
298
+ const compatible =
299
+ driver.id === "pgvector"
300
+ ? sqlDriver.id === "postgres" || sqlDriver.id === "pglite"
301
+ : sqlDriver.id === "libsql";
302
+ if (!compatible) {
303
+ throw new Error(
304
+ `oke store: index driver "${driver.id}" cannot share sql driver "${sqlDriver.id}" — ` +
305
+ (driver.id === "pgvector"
306
+ ? `pgvector needs store.sql on "postgres" or "pglite"`
307
+ : `libsql index needs store.sql on "libsql"`),
308
+ );
309
+ }
310
+ return sharedSqlConn(sqlDriver, "primary", { url });
311
+ }
312
+
234
313
  async function openKv(decl: KvStoreDecl): Promise<KvStoreFxHandle> {
235
314
  const driver = options.drivers.kv;
236
315
  if (!driver) throw new Error("No kv driver configured");
@@ -288,16 +367,30 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
288
367
  name: decl.name,
289
368
  dims: binding.dims ?? decl.dims ?? 3,
290
369
  url: binding.url,
291
- sql: binding.sql,
370
+ apiKey: binding.apiKey,
371
+ sql: indexDriverNeedsSql(driver)
372
+ ? (binding.sql ?? (await sqlConnForIndex(driver, binding.url)))
373
+ : undefined,
292
374
  });
293
375
  indexes.set(decl.name, idx);
294
376
  }
377
+ if (idx.driverId === "meilisearch") {
378
+ const text = idx;
379
+ return {
380
+ ref: `index:${decl.name}`,
381
+ driverId: text.driverId,
382
+ upsert: (id, document) => text.upsert(id, document),
383
+ search: (q, opts) => text.search(q, opts),
384
+ delete: (id) => text.delete(id),
385
+ };
386
+ }
387
+ const vector = idx as VectorIndexStore;
295
388
  return {
296
389
  ref: `index:${decl.name}`,
297
- driverId: idx.driverId,
298
- upsert: (id, vector, meta) => idx!.upsert(id, vector, meta),
299
- search: (vector, topK) => idx!.search(vector, topK),
300
- delete: (id) => idx!.delete(id),
390
+ driverId: vector.driverId,
391
+ upsert: (id, vec, meta) => vector.upsert(id, vec, meta),
392
+ search: (vec, topK) => vector.search(vec, topK),
393
+ delete: (id) => vector.delete(id),
301
394
  };
302
395
  }
303
396
 
@@ -55,6 +55,8 @@ export interface SchemaColumnDecl extends ColumnDef {
55
55
  readonly tableName?: string;
56
56
  /** Foreign key when `.references()` was used. */
57
57
  readonly references?: ColumnReference;
58
+ /** Optional human description for Console / docs (falls back to the JS key). */
59
+ readonly description?: string;
58
60
  }
59
61
 
60
62
  /** Table from {@link store.schema.table} — extends {@link TableHandle}. */
@@ -75,6 +77,8 @@ export interface FieldBuilder {
75
77
  retain(duration: string): FieldBuilder;
76
78
  /** Override snake_case SQL name. */
77
79
  as(sqlName: string): FieldBuilder;
80
+ /** Optional human description for Console / docs (falls back to the JS key). */
81
+ describe(description: string): FieldBuilder;
78
82
  /**
79
83
  * Declare a foreign key to another column (dialect-agnostic).
80
84
  *
@@ -100,6 +104,7 @@ interface FieldState {
100
104
  readonly defaultFnKind?: DefaultFnKind;
101
105
  readonly classification?: ColumnClassification;
102
106
  readonly sqlName?: string;
107
+ readonly description?: string;
103
108
  readonly references?: ColumnReference;
104
109
  }
105
110
 
@@ -145,6 +150,7 @@ function createBuilder(state: FieldState): FieldBuilder {
145
150
  retain: (duration) =>
146
151
  next({ classification: mergeClassification(state.classification, { retain: duration }) }),
147
152
  as: (sqlName) => next({ sqlName }),
153
+ describe: (description) => next({ description }),
148
154
  references: (ref, actions) =>
149
155
  next({
150
156
  references: actions ? { ref, actions } : { ref },
@@ -163,6 +169,7 @@ function createBuilder(state: FieldState): FieldBuilder {
163
169
  ...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
164
170
  ...(state.defaultFnKind ? { defaultFnKind: state.defaultFnKind } : {}),
165
171
  ...(state.classification ? { classification: state.classification } : {}),
172
+ ...(state.description !== undefined ? { description: state.description } : {}),
166
173
  ...(state.references ? { references: state.references } : {}),
167
174
  };
168
175
  },
@@ -116,6 +116,8 @@ export type {
116
116
  KvStoreFxHandle,
117
117
  FilesStoreFxHandle,
118
118
  IndexStoreFxHandle,
119
+ VectorIndexStoreFxHandle,
120
+ TextIndexStoreFxHandle,
119
121
  } from "./store/runtime.ts";
120
122
 
121
123
  export { fileKeyWarnings, contentAddressedKey, projectFileKeys } from "./store/files-policy.ts";
@@ -9,6 +9,7 @@
9
9
  import { describe, expect, test } from "bun:test";
10
10
  import { envVaultDriver, memoryVaultDriver } from "../drivers/index.ts";
11
11
  import { createFx } from "../kernel/fx.ts";
12
+ import { REDACTED_PLACEHOLDER, Redacted } from "../kernel/redacted.ts";
12
13
  import {
13
14
  createVaultRuntime,
14
15
  fingerprintSecretSync,
@@ -200,17 +201,39 @@ describe("redaction + fingerprints", () => {
200
201
  const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
201
202
  const { fx } = createFxContextWithVault(runtime, lines);
202
203
 
203
- const value = fx.vault("STRIPE_KEY");
204
- expect(value).toBe(secret);
204
+ const wrapped = fx.vault("STRIPE_KEY");
205
+ expect(wrapped.reveal()).toBe(secret);
205
206
 
206
- fx.log.info(`charging with ${value}`, { key: value, nested: { k: value } });
207
+ fx.log.info(`charging with ${wrapped}`, { key: wrapped, nested: { k: wrapped } });
208
+ // Revealed cleartext still hits the known-value substring scrub.
207
209
  fx.log.error(secret, { stripe: secret });
208
210
 
209
211
  for (const line of lines) {
210
212
  expect(line.message).not.toContain(secret);
211
213
  expect(JSON.stringify(line.data ?? {})).not.toContain(secret);
212
- expect(line.message).toContain(SECRET_MASK);
213
214
  }
215
+ expect(lines[0]!.message).toContain(REDACTED_PLACEHOLDER);
216
+ expect(lines[1]!.message).toContain(SECRET_MASK);
217
+ });
218
+
219
+ test("Redacted from fx.vault is masked in fx.log without a vault runtime", () => {
220
+ const secret = "sk_no_vault_runtime";
221
+ const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
222
+ const fx = createFx({
223
+ flow: "test.redacted",
224
+ secrets: { STRIPE_KEY: secret },
225
+ onLog: (_level, message, data) => lines.push({ message, data }),
226
+ });
227
+
228
+ const wrapped = fx.vault("STRIPE_KEY");
229
+ expect(wrapped).toBeInstanceOf(Redacted);
230
+ expect(String(wrapped)).toBe(REDACTED_PLACEHOLDER);
231
+ expect(`${wrapped}`).toBe(REDACTED_PLACEHOLDER);
232
+ expect(JSON.stringify({ v: wrapped })).toBe(`{"v":"${REDACTED_PLACEHOLDER}"}`);
233
+
234
+ fx.log.info("paying", { key: wrapped, nested: { k: [wrapped] } });
235
+ expect(JSON.stringify(lines[0]!.data)).not.toContain(secret);
236
+ expect(lines[0]!.data?.key).toBe(REDACTED_PLACEHOLDER);
214
237
  });
215
238
 
216
239
  test("fingerprint is stable and not the value", async () => {