@klum-db/lobby 0.2.0-pre.29 → 0.2.0-pre.31

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/README.md CHANGED
@@ -1,43 +1,65 @@
1
1
  # @klum-db/lobby
2
2
 
3
- > **The Lobby** orchestrates a *group* of sovereign [noy-db](https://github.com/vLannaAi/noy-db) vaults — federation, interchange, custody, and scoped sync — without ever dissolving their independence.
4
- >
5
- > **noy-db is the vault (inward). klum-db is the Lobby (outward).**
6
- > A container runs perfectly alone; the engine orchestrates many. Docker is to a container what the Lobby is to a vault.
3
+ > **Small enough to own, coordinated enough to scale.**
4
+ > `@klum-db/lobby` is the **control plane** for a fleet of sovereign [noy-db](https://github.com/vLannaAi/noy-db) vaults. Each vault is small, **100%-encrypted**, and complete on its own — its own embedded schema, its own query engine, usable **offline**. The Lobby orchestrates many into one coordinated whole, **online or offline**: **coordination without custody** — it drives the fleet but never owns the data. *One-way — klum drives noy; noy never knows klum exists.*
7
5
 
8
6
  `@klum-db/lobby` · status: **preview** · depends on `@noy-db/hub`
9
7
 
10
8
  ---
11
9
 
12
- ## The idea in 20 seconds
10
+ ## What it is, in 20 seconds
13
11
 
14
- Banking, accounting, health, insurancethe data that matters is **individual, single-entity, and owned by the subject**. noy-db makes each of those a small, sovereign, in-memory vault that is a *complete system on its own*. Working "small, in memory" isn't a limitation it's the strong core. The limitation only bites when one actor must work across **many** datasets at once.
12
+ A single vault is complete its own keys, schema, and truth but completeness is also a ceiling: alone, a vault can only answer for *itself*. The usual fix is to pour everything into one central store, trading away the sovereignty that made each vault worth trusting. **The Lobby takes the other path it coordinates the vaults where they stand.**
15
13
 
16
- The **Lobby** is the framework for exactly that outward dimension: *the efficiency of a small independent dataset at the core, joined with an actor operating across many at the same time* — a counterbalance to tech giants holding user data hostage. Vaults stay independent and relocatable; the Lobby coordinates them without absorbing them. That's why it's a **group** (Thai *klum* กลุ่ม), never a cluster — federation here is non-aggregative.
17
-
18
- ## Reads in a sentence
19
-
20
- > A firm is a **Custodian** in the **Lobby**: it holds operating grants to client **Vaults** that all reference one shared **Pool**. The client holds the **Deed**. To onboard, the firm **Relocates** a client's vault as a **Bundle**, **Migrates** it to the current schema, and **Merges** the Pool slice by field **Authority** using **Provenance**. The client can **Withdraw** anytime; an abandoned vault can be **Liberated**.
14
+ ```mermaid
15
+ flowchart TD
16
+ L["<b>CONTROL PLANE</b> · @klum-db/lobby<br/>federate · interchange · custody · surface"]:::cp
17
+ L ==> V1["🔒 vault"]:::dp
18
+ L ==> V2["🔒 vault"]:::dp
19
+ L ==> V3["🔒 vault"]:::dp
20
+ L ==> V4["🔒 vault"]:::dp
21
+ classDef cp fill:#0f766e,stroke:#0f766e,color:#ffffff
22
+ classDef dp fill:#ecfdf5,stroke:#10b981,color:#065f46
23
+ ```
21
24
 
22
- Every bold word is a real, shipped capability.
25
+ *DATA PLANE · `@noy-db/hub` each vault sovereign & 100% encrypted, complete on its own. **klum drives noy one-way; noy never depends on klum.***
23
26
 
24
- ## Architecture one-way dependency, two complementary axes
27
+ <details><summary>Text version (npm / non-Mermaid viewers)</summary>
25
28
 
26
29
  ```
27
- outward / orchestration inward / the vault
28
- ┌─────────────────────────────────┐ ┌──────────────────────────────────┐
29
- @klum-db/lobby │ @noy-db/hub │
30
- │ │ │ │
31
- Lobby ⊃ many Vaults │ ──▶ │ Vault Collection ⊃ Record ⊃ Field │
32
- │ • Federation (fleets) │ │ • keyring · per-record CEK │
33
- │ • Interchange (move data) │ │ • computed/derivation · money │
34
- • Custody (Deed/Custodian) │ │ • i18n · history · snapshots │
35
- Surface (scoped sync) • a vault + a store = complete
36
- └─────────────────────────────────┘ └──────────────────────────────────┘
37
- binds to the stable ── @noy-db/hub/kernel ── surface only
30
+ ┌────────────────────────────────────────────────────┐
31
+ │ CONTROL PLANE · @klum-db/lobby │
32
+ federate · interchange · custody · surface
33
+ └────────────────────────┬───────────────────────────┘
34
+ drives one-way (klum noy)
35
+ ┌──────────┬───────┴───────┬──────────┐
36
+ ▼ ▼ ▼ ▼
37
+ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
38
+ │vault │vault │ │vault │vault │
39
+ └──────┘ └──────┘ └──────┘ └──────┘
40
+ DATA PLANE · @noy-db/hub each vault sovereign & non-fungible,
41
+ complete on its own. noy never depends on klum.
38
42
  ```
43
+ </details>
44
+
45
+ - **Coordination without custody.** klum drives the fleet — federate, move data, custody, sync — but **never owns the data**: keys, crypto, and records stay sovereign in each vault, and klum binds to one stable contract (`@noy-db/hub/kernel`), never hub internals. *(One-way, and enforced — a build-time guard means no `@noy-db` package can ever import `@klum-db`.)*
46
+ - **Bring the work to the data, not the data to a lake.** Cross-vault queries resolve *across* the group — each vault answers for its own slice under its own keys, nothing pools. No central honeypot to breach.
47
+ - **Small core, coordinated reach.** Each vault stays small, portable, and individually revocable; orchestration recovers the cross-cutting reach you'd otherwise need a monolith for.
48
+ - **A group, not a cluster.** Vaults are sovereign and non-fungible — one subject = one vault, the subject holds the deed. *Joined, not merged; allied, not absorbed.* (Thai *klum* กลุ่ม = a group.)
49
+
50
+ ## Why it exists
51
+
52
+ Banking, accounting, health, insurance — the data that matters is **individual, single-subject, and owned by the person it's about**. noy-db makes each of those a small, sovereign, **100%-encrypted** vault — a complete system on its own. *You own your data, in spite of the cloud.* The limit only bites when one actor must work across **many** subjects at once.
53
+
54
+ That's the Lobby's dimension: the efficiency and privacy of a small sovereign dataset at the core, joined with an actor operating across many at once — **governance by default, not by checklist**. Access is **scoped, purpose-limited, and revocable** (the subject can withdraw anytime); the orchestrator coordinates without absorbing. A counterweight to the central data lake — without the lock-in, the lock-out, or the single honeypot.
39
55
 
40
- The dependency runs **one way**: `@klum-db/lobby` `@noy-db/hub`. No `@noy-db` package ever imports `@klum-db` (enforced by a build-time architecture guard). The Lobby binds to a stable internal surface, **`@noy-db/hub/kernel`**, not hub internals. A vault is a complete, shippable system *without* the Lobby; the Lobby is what you reach for when one actor must work across many vaults at once.
56
+ **Lineage & market context:** klum-db sits in the local-first / data-sovereignty tradition [Local-first software](https://www.inkandswitch.com/essay/local-first/), [GDPR data portability](https://gdpr-info.eu/art-20-gdpr/), and the full picture in [**docs/positioning.md**](docs/positioning.md).
57
+
58
+ ## Reads in a sentence
59
+
60
+ > A firm is a **Custodian** in the **Lobby**: it holds operating grants to client **Vaults** that all reference one shared **Pool**. The client holds the **Deed**. To onboard, the firm **Relocates** a client's vault as a **Bundle**, **Migrates** it to the current schema, and **Merges** the Pool slice by field **Authority** using **Provenance**. The client can **Withdraw** anytime; an abandoned vault can be **Liberated**.
61
+
62
+ Every bold word is a real, shipped capability.
41
63
 
42
64
  ## Install
43
65
 
@@ -131,17 +153,22 @@ const { bundleBytes, transferKey } = await lobby.exportSurface('payroll-vault',
131
153
  await lobby.applySurface('tax-vault', surface, bundleBytes, transferKey)
132
154
  ```
133
155
 
156
+ ### On-ramp · Dock → `graduate()`
157
+
158
+ A foreign, non-noy-db unit (a legacy DB, a raw export) can't be federated directly — the sovereign tier needs a vault's keyring and per-record keys. **Dock** carries it read-only at a lower tier; **`graduate()`** imports it into a fresh sovereign vault, unlocking the full tier (custody, provenance, field-Authority merge). It's the one move that *adds a node to the data plane* rather than coordinating existing ones.
159
+
134
160
  ---
135
161
 
136
162
  ## Relationship with noy-db
137
163
 
138
- - **Depends on `@noy-db/hub`**, binds to the stable **`@noy-db/hub/kernel`** subpathnever reaches into hub internals.
164
+ The one-way law and the kernel seam are covered up tophere are the specifics that matter when you build:
165
+
139
166
  - **Custody is a vault-level concern** and lives *in* hub (keyring/CEK/consent primitives); the Lobby **re-exports** it (`createDeedOwner`, `liberateVault`, `CustodyApi`) so consumers have one import surface.
140
167
  - **Federation** lives in the Lobby, not in hub — open fleets with `lobby.openVaultGroup` (`@noy-db/hub` no longer ships the `openVaultGroup` / `openStateManagementVault` / `withVaultTemplate` fleet methods).
141
- - The dependency is enforced one-way at build time; an `@noy-db` package importing `@klum-db` fails the architecture check.
168
+ - **Enforced, not conventional:** an `@noy-db` package importing `@klum-db` fails noy-db's build-time architecture check.
142
169
 
143
170
  ## Status
144
171
 
145
172
  Preview. `@klum-db/lobby` is its own repository and the sole publisher of `@klum-db/*` to npm. It depends on the **published** `@noy-db/*` packages through the stable `@noy-db/hub/kernel` boundary and versions **independently** (`0.2.0-pre.N`, decoupled from noy-db). Pilot-1 (FR-1…FR-9), the dock tier, and `Lobby.graduate()` are complete.
146
173
 
147
- See [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
174
+ See [`docs/architecture.md`](docs/architecture.md) for the detailed noy-db ↔ klum-db boundary, [`docs/roadmap.md`](docs/roadmap.md) for what's next, and [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
@@ -1,4 +1,4 @@
1
- import { V as VaultGroup } from '../vault-group-BXjO5kHB.cjs';
1
+ import { V as VaultGroup } from '../vault-group-DqEyXbN1.cjs';
2
2
  import '@noy-db/hub/kernel';
3
3
  import '@noy-db/hub';
4
4
  import '@noy-db/hub/bundle';
@@ -1,4 +1,4 @@
1
- import { V as VaultGroup } from '../vault-group-BXjO5kHB.js';
1
+ import { V as VaultGroup } from '../vault-group-DqEyXbN1.js';
2
2
  import '@noy-db/hub/kernel';
3
3
  import '@noy-db/hub';
4
4
  import '@noy-db/hub/bundle';
package/dist/index.cjs CHANGED
@@ -1268,6 +1268,43 @@ var init_insight_auto_push = __esm({
1268
1268
  }
1269
1269
  });
1270
1270
 
1271
+ // src/federation/partial-reduce.ts
1272
+ function canPartialReduce(spec) {
1273
+ return Object.values(spec).every((r) => typeof r.merge === "function");
1274
+ }
1275
+ function reduceToPartial(records, spec) {
1276
+ const out = {};
1277
+ for (const [key, reducer] of Object.entries(spec)) {
1278
+ const r = reducer;
1279
+ let state = r.init();
1280
+ for (const rec of records) state = r.step(state, rec);
1281
+ out[key] = state;
1282
+ }
1283
+ return out;
1284
+ }
1285
+ function mergePartials(spec, partials) {
1286
+ const out = {};
1287
+ for (const [key, reducer] of Object.entries(spec)) {
1288
+ const r = reducer;
1289
+ let acc = r.init();
1290
+ for (const p of partials) acc = r.merge(acc, p[key]);
1291
+ out[key] = acc;
1292
+ }
1293
+ return out;
1294
+ }
1295
+ function finalizePartial(spec, merged) {
1296
+ const out = {};
1297
+ for (const [key, reducer] of Object.entries(spec)) {
1298
+ out[key] = reducer.finalize(merged[key]);
1299
+ }
1300
+ return out;
1301
+ }
1302
+ var init_partial_reduce = __esm({
1303
+ "src/federation/partial-reduce.ts"() {
1304
+ "use strict";
1305
+ }
1306
+ });
1307
+
1271
1308
  // src/federation/aggregate-across.ts
1272
1309
  var import_kernel8, import_kernel9, CrossVaultAggregation, CrossVaultGroupedAggregation;
1273
1310
  var init_aggregate_across = __esm({
@@ -1276,6 +1313,7 @@ var init_aggregate_across = __esm({
1276
1313
  import_kernel8 = require("@noy-db/hub/kernel");
1277
1314
  import_kernel9 = require("@noy-db/hub/kernel");
1278
1315
  init_cross_vault_live();
1316
+ init_partial_reduce();
1279
1317
  CrossVaultAggregation = class {
1280
1318
  constructor(src, spec, bind) {
1281
1319
  this.src = src;
@@ -1286,6 +1324,10 @@ var init_aggregate_across = __esm({
1286
1324
  spec;
1287
1325
  bind;
1288
1326
  async run(options = {}) {
1327
+ if (this.src.fanoutReduce && canPartialReduce(this.spec)) {
1328
+ const { partials, skippedVaults: skippedVaults2 } = await this.src.fanoutReduce(this.spec, options);
1329
+ return { result: finalizePartial(this.spec, mergePartials(this.spec, partials)), skippedVaults: skippedVaults2 };
1330
+ }
1289
1331
  const { records, skippedVaults } = await this.src.fanoutRecords(options);
1290
1332
  return { result: (0, import_kernel8.reduceRecords)(records, this.spec), skippedVaults };
1291
1333
  }
@@ -1412,6 +1454,7 @@ var init_vault_group = __esm({
1412
1454
  init_cross_vault_live();
1413
1455
  init_insight_auto_push();
1414
1456
  init_aggregate_across();
1457
+ init_partial_reduce();
1415
1458
  SHARD_SEPARATOR = "--";
1416
1459
  SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
1417
1460
  VaultGroup = class {
@@ -1550,22 +1593,33 @@ var init_vault_group = __esm({
1550
1593
  collection(collectionName) {
1551
1594
  return new ShardedCollection(this, collectionName);
1552
1595
  }
1553
- /** @internal — eligible (openable-candidate) rows + drift/divergence skips. */
1596
+ /** @internal — eligible (openable-candidate) rows + drift/divergence/unreachable skips. */
1554
1597
  async resolveEligible(options = {}) {
1555
1598
  const rows = await this.allRows();
1599
+ const candidates = options.only ? rows.filter((r) => options.only.includes(r.partitionKey)) : rows;
1556
1600
  const skipped = [];
1557
1601
  const versionOk = [];
1558
- for (const row of rows) {
1602
+ for (const row of candidates) {
1559
1603
  if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
1560
1604
  skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
1561
1605
  } else versionOk.push(row);
1562
1606
  }
1563
- const provisioned = await Promise.all(versionOk.map((r) => this.db._shardVaultProvisioned(r.vaultId)));
1607
+ const probes = await Promise.all(
1608
+ versionOk.map(async (row) => {
1609
+ try {
1610
+ return { row, provisioned: await this.db._shardVaultProvisioned(row.vaultId) };
1611
+ } catch (err) {
1612
+ if (options.failFast) throw err;
1613
+ return { row, error: err };
1614
+ }
1615
+ })
1616
+ );
1564
1617
  const eligible = [];
1565
- versionOk.forEach((row, i) => {
1566
- if (provisioned[i]) eligible.push(row);
1567
- else skipped.push({ vaultId: row.vaultId, reason: "error", error: new import_kernel10.ShardProvisioningError(row.vaultId, row.partitionKey) });
1568
- });
1618
+ for (const p of probes) {
1619
+ if ("error" in p) skipped.push({ vaultId: p.row.vaultId, reason: "error", error: p.error });
1620
+ else if (p.provisioned) eligible.push(p.row);
1621
+ else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_kernel10.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
1622
+ }
1569
1623
  return { eligible, skipped };
1570
1624
  }
1571
1625
  /** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
@@ -1625,12 +1679,19 @@ var init_vault_group = __esm({
1625
1679
  * Insight Vault keyed by partition key. Shards behind `minVersion`,
1626
1680
  * unprovisioned, or whose read errors are reported in `skippedVaults` and
1627
1681
  * are not written (a stale summary is never left behind for a failed shard).
1682
+ * A shard whose backend is unreachable (provisioning probe or read throws) is
1683
+ * **skipped** (`reason: 'error'`) and the pass continues for the others — its
1684
+ * prior summary is left intact. Pass `failFast: true` for the legacy
1685
+ * all-or-nothing throw. Reconcile a lagging shard later with
1686
+ * `refreshInsights({ only: [pk] })` or `refreshDerivation(pk)`.
1628
1687
  */
1629
1688
  async refreshInsights(options = {}) {
1630
1689
  if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
1631
- const { eligible, skipped } = await this.resolveEligible(
1632
- options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
1633
- );
1690
+ const { eligible, skipped } = await this.resolveEligible({
1691
+ ...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
1692
+ ...options.only !== void 0 ? { only: options.only } : {},
1693
+ ...options.failFast !== void 0 ? { failFast: options.failFast } : {}
1694
+ });
1634
1695
  let written = 0;
1635
1696
  for (const spec of this.crossVaultDerivations) {
1636
1697
  const results = await this.db.queryAcross(
@@ -1647,6 +1708,7 @@ var init_vault_group = __esm({
1647
1708
  const row = eligible[i];
1648
1709
  const res = results[i];
1649
1710
  if (!res || res.result === void 0) {
1711
+ if (options.failFast && res?.error) throw res.error;
1650
1712
  skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
1651
1713
  continue;
1652
1714
  }
@@ -1662,6 +1724,14 @@ var init_vault_group = __esm({
1662
1724
  }
1663
1725
  return { written, skippedVaults: skipped };
1664
1726
  }
1727
+ /**
1728
+ * Reconcile one shard's Insight summaries after its backend was unreachable.
1729
+ * Equivalent to `refreshInsights({ only: [partitionKey] })` — runs every
1730
+ * registered derivation (autoPush or not) for just this shard.
1731
+ */
1732
+ async refreshDerivation(partitionKey) {
1733
+ return this.refreshInsights({ only: [partitionKey] });
1734
+ }
1665
1735
  /** @internal — re-derive + push every autoPush derivation's summary for one shard. */
1666
1736
  async _recomputeShardInsights(partitionKey) {
1667
1737
  const row = await this.registry.get(this.registryId(partitionKey));
@@ -1897,6 +1967,34 @@ var init_vault_group = __esm({
1897
1967
  }
1898
1968
  return { records: results, skippedVaults: skipped };
1899
1969
  }
1970
+ /**
1971
+ * @internal — distributed partial-reduce (#8). Fan out across eligible shards,
1972
+ * but fold each shard's where-filtered records to a partial reducer STATE
1973
+ * inside the per-shard callback (never returning the rows). Only the scalar
1974
+ * `.aggregate()` path calls this, and that path rejects join legs upstream.
1975
+ */
1976
+ async fanoutReduce(spec, options = {}) {
1977
+ const { eligible, skipped } = await this.group.resolveEligible(options);
1978
+ const across = await this.group.db.queryAcross(
1979
+ eligible.map((r) => r.vaultId),
1980
+ async (vault) => {
1981
+ this.group.template.configure(vault);
1982
+ const coll = vault.collection(this.collectionName);
1983
+ await coll.list();
1984
+ let q = coll.query();
1985
+ for (const c of this.clauses) q = q.where(c.field, c.op, c.value);
1986
+ const rows = q.toArray();
1987
+ return reduceToPartial(rows, spec);
1988
+ },
1989
+ { concurrency: options.concurrency ?? 1, create: false }
1990
+ );
1991
+ const partials = [];
1992
+ for (const r of across) {
1993
+ if (r.error) skipped.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
1994
+ else partials.push(r.result);
1995
+ }
1996
+ return { partials, skippedVaults: skipped };
1997
+ }
1900
1998
  /** Fan out across eligible shards, merge, then apply any broadcast dimension legs. */
1901
1999
  async toArray(options = {}) {
1902
2000
  const { records, skippedVaults } = await this.fanoutRecords(options);