@klum-db/lobby 0.2.0-pre.30 → 0.2.0-pre.32

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-BchRCDm5.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-BchRCDm5.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
  }
@@ -1374,6 +1416,73 @@ var init_aggregate_across = __esm({
1374
1416
  }
1375
1417
  });
1376
1418
 
1419
+ // src/federation/retrieve-across.ts
1420
+ async function retrieveAcross(group, collectionName, query, opts = {}) {
1421
+ const { eligible, skipped } = await group.resolveEligible({
1422
+ ...opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {},
1423
+ ...opts.failFast !== void 0 ? { failFast: opts.failFast } : {}
1424
+ });
1425
+ const across = await group.db.queryAcross(
1426
+ eligible.map((r) => r.vaultId),
1427
+ async (vault) => {
1428
+ group.template.configure(vault);
1429
+ const coll = vault.collection(collectionName);
1430
+ let within;
1431
+ if (opts.where?.length) {
1432
+ let q = coll.query();
1433
+ for (const [f, op, v] of opts.where) q = q.where(f, op, v);
1434
+ within = q;
1435
+ }
1436
+ return coll.retrieve(query, {
1437
+ ...opts.mode ? { mode: opts.mode } : {},
1438
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
1439
+ ...opts.minScore !== void 0 ? { minScore: opts.minScore } : {},
1440
+ ...opts.fields ? { fields: opts.fields } : {},
1441
+ ...opts.match ? { match: opts.match } : {},
1442
+ ...opts.prefix ? { prefix: opts.prefix } : {},
1443
+ ...opts.snippetWindow !== void 0 ? { snippetWindow: opts.snippetWindow } : {},
1444
+ ...opts.includeRecord ? { includeRecord: true } : {},
1445
+ ...within ? { within } : {}
1446
+ });
1447
+ },
1448
+ { concurrency: opts.concurrency ?? 1, create: false }
1449
+ );
1450
+ const skippedVaults = [...skipped];
1451
+ const perVault = [];
1452
+ for (const r of across) {
1453
+ if (r.error) {
1454
+ if (opts.failFast) throw r.error;
1455
+ skippedVaults.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
1456
+ } else {
1457
+ perVault.push({ vault: r.vault, hits: r.result });
1458
+ }
1459
+ }
1460
+ const lists = perVault.map((pv) => {
1461
+ if (pv.vault.includes(SEP)) {
1462
+ throw new Error(`retrieveAcross: vault id "${pv.vault}" contains the reserved NUL separator`);
1463
+ }
1464
+ return pv.hits.map((h) => ({ ...h, id: pv.vault + SEP + h.id }));
1465
+ });
1466
+ const fused = (0, import_kernel10.fuseRetrieval)(lists, {
1467
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
1468
+ ...opts.rrfK !== void 0 ? { k: opts.rrfK } : {}
1469
+ });
1470
+ const hits = fused.map((h) => {
1471
+ const i = h.id.indexOf(SEP);
1472
+ return { ...h, vault: h.id.slice(0, i), id: h.id.slice(i + 1) };
1473
+ });
1474
+ return { hits, skippedVaults };
1475
+ }
1476
+ var import_kernel10, SEP;
1477
+ var init_retrieve_across = __esm({
1478
+ "src/federation/retrieve-across.ts"() {
1479
+ "use strict";
1480
+ import_kernel10 = require("@noy-db/hub/kernel");
1481
+ init_classify_skip();
1482
+ SEP = "\0";
1483
+ }
1484
+ });
1485
+
1377
1486
  // src/federation/vault-group.ts
1378
1487
  var vault_group_exports = {};
1379
1488
  __export(vault_group_exports, {
@@ -1384,46 +1493,49 @@ __export(vault_group_exports, {
1384
1493
  });
1385
1494
  function assertSafePartitionKey(partitionKey) {
1386
1495
  if (partitionKey.length === 0) {
1387
- throw new import_kernel10.ValidationError("partitionKey must be a non-empty string");
1496
+ throw new import_kernel11.ValidationError("partitionKey must be a non-empty string");
1388
1497
  }
1389
1498
  if (partitionKey === import_hub4.STATE_VAULT_NAME) {
1390
- throw new import_kernel10.ReservedVaultNameError(partitionKey);
1499
+ throw new import_kernel11.ReservedVaultNameError(partitionKey);
1391
1500
  }
1392
1501
  if (!SAFE_PARTITION_KEY.test(partitionKey)) {
1393
- throw new import_kernel10.ValidationError(
1502
+ throw new import_kernel11.ValidationError(
1394
1503
  `partitionKey "${partitionKey}" contains characters outside [A-Za-z0-9._-]. Map your records to a store-safe key in sharding.keyOf.`
1395
1504
  );
1396
1505
  }
1397
1506
  if (partitionKey.includes(SHARD_SEPARATOR)) {
1398
- throw new import_kernel10.ValidationError(
1507
+ throw new import_kernel11.ValidationError(
1399
1508
  `partitionKey "${partitionKey}" must not contain "--" \u2014 it is reserved as the shard vault-id separator and would risk shard-id collisions.`
1400
1509
  );
1401
1510
  }
1402
1511
  }
1403
- var import_kernel10, SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQuery, ShardedGroupedQuery;
1512
+ var import_kernel11, SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQuery, ShardedGroupedQuery;
1404
1513
  var init_vault_group = __esm({
1405
1514
  "src/federation/vault-group.ts"() {
1406
1515
  "use strict";
1407
1516
  init_state_vault();
1408
- import_kernel10 = require("@noy-db/hub/kernel");
1517
+ import_kernel11 = require("@noy-db/hub/kernel");
1409
1518
  init_constants();
1410
1519
  init_classify_skip();
1411
1520
  init_cross_shard_join();
1412
1521
  init_cross_vault_live();
1413
1522
  init_insight_auto_push();
1414
1523
  init_aggregate_across();
1524
+ init_retrieve_across();
1525
+ init_partial_reduce();
1415
1526
  SHARD_SEPARATOR = "--";
1416
1527
  SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
1417
1528
  VaultGroup = class {
1418
- constructor(db, name, registry, sharding, template, cutoverOnOpen = false) {
1529
+ constructor(db, name, registry, sharding, template, cutoverOnOpen = false, meta) {
1419
1530
  this.db = db;
1420
1531
  this.name = name;
1421
1532
  this.registry = registry;
1422
1533
  this.sharding = sharding;
1423
1534
  this.template = template;
1424
1535
  this.cutoverOnOpen = cutoverOnOpen;
1536
+ this.meta = meta;
1425
1537
  if (name.includes(SHARD_SEPARATOR)) {
1426
- throw new import_kernel10.ValidationError(
1538
+ throw new import_kernel11.ValidationError(
1427
1539
  `VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
1428
1540
  );
1429
1541
  }
@@ -1434,6 +1546,7 @@ var init_vault_group = __esm({
1434
1546
  sharding;
1435
1547
  template;
1436
1548
  cutoverOnOpen;
1549
+ meta;
1437
1550
  /** @internal — set when the group is managed (no explicit registry). */
1438
1551
  stateVault;
1439
1552
  /** @internal */
@@ -1467,6 +1580,30 @@ var init_vault_group = __esm({
1467
1580
  const rows = this.registry.query().toArray();
1468
1581
  return rows.filter((r) => r.group === this.name);
1469
1582
  }
1583
+ /**
1584
+ * Federation-level descriptive metadata (#27): this group's `meta` plus each
1585
+ * member vault's `vaultMeta` (read via `getMeta()`). Best-effort per shard — a
1586
+ * shard that fails to open yields `meta: undefined` for that entry, never
1587
+ * throwing. Member meta is transient per-open (noy-db sets it at `openVault`,
1588
+ * does not persist it); this surfaces whatever each vault was opened with.
1589
+ */
1590
+ async federationMeta() {
1591
+ const rows = await this.allRows();
1592
+ const vaults = await Promise.all(
1593
+ rows.map(async (row) => {
1594
+ let meta;
1595
+ try {
1596
+ const vault = await this.db.openVault(row.vaultId);
1597
+ this.template.configure(vault);
1598
+ meta = vault.getMeta();
1599
+ } catch {
1600
+ meta = void 0;
1601
+ }
1602
+ return { vaultId: row.vaultId, partitionKey: row.partitionKey, meta };
1603
+ })
1604
+ );
1605
+ return { meta: this.meta, vaults };
1606
+ }
1470
1607
  /**
1471
1608
  * Open an existing shard and apply the template. When `cutoverOnOpen` is set
1472
1609
  * (#271) and the shard's registry version is behind the template, its cutover
@@ -1503,11 +1640,11 @@ var init_vault_group = __esm({
1503
1640
  const vaultId = this.shardVaultId(partitionKey);
1504
1641
  const row = await this.registry.get(this.registryId(partitionKey));
1505
1642
  const provisioned = await this.db._shardVaultProvisioned(vaultId);
1506
- if (row && !provisioned) throw new import_kernel10.ShardProvisioningError(vaultId, partitionKey);
1643
+ if (row && !provisioned) throw new import_kernel11.ShardProvisioningError(vaultId, partitionKey);
1507
1644
  if (row && provisioned) return this.openShard(partitionKey);
1508
1645
  if (region !== void 0) {
1509
1646
  const backendRegion = this.db._resolveBackend(vaultId).capabilities?.region;
1510
- if (backendRegion !== region) throw new import_kernel10.DataResidencyError(vaultId, region, backendRegion);
1647
+ if (backendRegion !== region) throw new import_kernel11.DataResidencyError(vaultId, region, backendRegion);
1511
1648
  }
1512
1649
  const vault = await this.db.openVault(vaultId);
1513
1650
  this.template.configure(vault);
@@ -1541,31 +1678,42 @@ var init_vault_group = __esm({
1541
1678
  async shard(partitionKey) {
1542
1679
  const vaultId = this.shardVaultId(partitionKey);
1543
1680
  const row = await this.registry.get(this.registryId(partitionKey));
1544
- if (!row) throw new import_kernel10.UnknownShardError(partitionKey, this.name);
1681
+ if (!row) throw new import_kernel11.UnknownShardError(partitionKey, this.name);
1545
1682
  const provisioned = await this.db._shardVaultProvisioned(vaultId);
1546
- if (!provisioned) throw new import_kernel10.ShardProvisioningError(vaultId, partitionKey);
1683
+ if (!provisioned) throw new import_kernel11.ShardProvisioningError(vaultId, partitionKey);
1547
1684
  return this.openShard(partitionKey);
1548
1685
  }
1549
1686
  /** A sharded view over one logical collection across all shards. */
1550
1687
  collection(collectionName) {
1551
1688
  return new ShardedCollection(this, collectionName);
1552
1689
  }
1553
- /** @internal — eligible (openable-candidate) rows + drift/divergence skips. */
1690
+ /** @internal — eligible (openable-candidate) rows + drift/divergence/unreachable skips. */
1554
1691
  async resolveEligible(options = {}) {
1555
1692
  const rows = await this.allRows();
1693
+ const candidates = options.only ? rows.filter((r) => options.only.includes(r.partitionKey)) : rows;
1556
1694
  const skipped = [];
1557
1695
  const versionOk = [];
1558
- for (const row of rows) {
1696
+ for (const row of candidates) {
1559
1697
  if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
1560
1698
  skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
1561
1699
  } else versionOk.push(row);
1562
1700
  }
1563
- const provisioned = await Promise.all(versionOk.map((r) => this.db._shardVaultProvisioned(r.vaultId)));
1701
+ const probes = await Promise.all(
1702
+ versionOk.map(async (row) => {
1703
+ try {
1704
+ return { row, provisioned: await this.db._shardVaultProvisioned(row.vaultId) };
1705
+ } catch (err) {
1706
+ if (options.failFast) throw err;
1707
+ return { row, error: err };
1708
+ }
1709
+ })
1710
+ );
1564
1711
  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
- });
1712
+ for (const p of probes) {
1713
+ if ("error" in p) skipped.push({ vaultId: p.row.vaultId, reason: "error", error: p.error });
1714
+ else if (p.provisioned) eligible.push(p.row);
1715
+ else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_kernel11.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
1716
+ }
1569
1717
  return { eligible, skipped };
1570
1718
  }
1571
1719
  /** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
@@ -1601,7 +1749,7 @@ var init_vault_group = __esm({
1601
1749
  withCrossVaultDerivation(spec) {
1602
1750
  const target = spec.target.vault;
1603
1751
  if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
1604
- throw new import_kernel10.ValidationError(
1752
+ throw new import_kernel11.ValidationError(
1605
1753
  `withCrossVaultDerivation: target.vault "${target}" is the "${this.name}" group itself or one of its shards \u2014 an Insight summary must target a SEPARATE analytics vault, never write back into client-shard data (it would breach the per-shard DEK boundary). Use a distinct vault name.`
1606
1754
  );
1607
1755
  }
@@ -1625,12 +1773,19 @@ var init_vault_group = __esm({
1625
1773
  * Insight Vault keyed by partition key. Shards behind `minVersion`,
1626
1774
  * unprovisioned, or whose read errors are reported in `skippedVaults` and
1627
1775
  * are not written (a stale summary is never left behind for a failed shard).
1776
+ * A shard whose backend is unreachable (provisioning probe or read throws) is
1777
+ * **skipped** (`reason: 'error'`) and the pass continues for the others — its
1778
+ * prior summary is left intact. Pass `failFast: true` for the legacy
1779
+ * all-or-nothing throw. Reconcile a lagging shard later with
1780
+ * `refreshInsights({ only: [pk] })` or `refreshDerivation(pk)`.
1628
1781
  */
1629
1782
  async refreshInsights(options = {}) {
1630
1783
  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
- );
1784
+ const { eligible, skipped } = await this.resolveEligible({
1785
+ ...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
1786
+ ...options.only !== void 0 ? { only: options.only } : {},
1787
+ ...options.failFast !== void 0 ? { failFast: options.failFast } : {}
1788
+ });
1634
1789
  let written = 0;
1635
1790
  for (const spec of this.crossVaultDerivations) {
1636
1791
  const results = await this.db.queryAcross(
@@ -1647,6 +1802,7 @@ var init_vault_group = __esm({
1647
1802
  const row = eligible[i];
1648
1803
  const res = results[i];
1649
1804
  if (!res || res.result === void 0) {
1805
+ if (options.failFast && res?.error) throw res.error;
1650
1806
  skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
1651
1807
  continue;
1652
1808
  }
@@ -1662,6 +1818,14 @@ var init_vault_group = __esm({
1662
1818
  }
1663
1819
  return { written, skippedVaults: skipped };
1664
1820
  }
1821
+ /**
1822
+ * Reconcile one shard's Insight summaries after its backend was unreachable.
1823
+ * Equivalent to `refreshInsights({ only: [partitionKey] })` — runs every
1824
+ * registered derivation (autoPush or not) for just this shard.
1825
+ */
1826
+ async refreshDerivation(partitionKey) {
1827
+ return this.refreshInsights({ only: [partitionKey] });
1828
+ }
1665
1829
  /** @internal — re-derive + push every autoPush derivation's summary for one shard. */
1666
1830
  async _recomputeShardInsights(partitionKey) {
1667
1831
  const row = await this.registry.get(this.registryId(partitionKey));
@@ -1706,7 +1870,7 @@ var init_vault_group = __esm({
1706
1870
  async cutoverShard(partitionKey) {
1707
1871
  const vaultId = this.shardVaultId(partitionKey);
1708
1872
  const row = await this.registry.get(this.registryId(partitionKey));
1709
- if (!row) throw new import_kernel10.UnknownShardError(partitionKey, this.name);
1873
+ if (!row) throw new import_kernel11.UnknownShardError(partitionKey, this.name);
1710
1874
  const target = this.template.version;
1711
1875
  const sv = await this.ensureStateVault();
1712
1876
  const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
@@ -1789,7 +1953,7 @@ var init_vault_group = __esm({
1789
1953
  let vault;
1790
1954
  if (!row) {
1791
1955
  if (this.group.sharding.autoCreate === false) {
1792
- throw new import_kernel10.UnknownShardError(key, this.group.name);
1956
+ throw new import_kernel11.UnknownShardError(key, this.group.name);
1793
1957
  }
1794
1958
  vault = await this.group.createShard(key, this.group.sharding.regionOf?.(record));
1795
1959
  } else {
@@ -1801,6 +1965,17 @@ var init_vault_group = __esm({
1801
1965
  query() {
1802
1966
  return new ShardedQuery(this.group, this.collectionName, []);
1803
1967
  }
1968
+ /**
1969
+ * Cross-vault federated retrieval (#26): scatter-gather across all eligible
1970
+ * shards — each shard runs its own trusted-tier `retrieve()`, then results are
1971
+ * RRF-fused by rank only (no cross-vault statistics cross the DEK boundary).
1972
+ * Every hit carries its originating `vault`. An unreachable, schema-drifted, or
1973
+ * un-indexed shard is skipped (`skippedVaults`); pass `failFast` to re-throw
1974
+ * the first per-shard error instead.
1975
+ */
1976
+ async retrieve(query, opts = {}) {
1977
+ return retrieveAcross(this.group, this.collectionName, query, opts);
1978
+ }
1804
1979
  };
1805
1980
  ShardedQuery = class _ShardedQuery {
1806
1981
  constructor(group, collectionName, clauses, coPartitionedLegs = [], broadcastLegs = []) {
@@ -1861,7 +2036,7 @@ var init_vault_group = __esm({
1861
2036
  this.group.template.configure(probe);
1862
2037
  for (const leg of this.coPartitionedLegs) {
1863
2038
  if (!probe.resolveRef(this.collectionName, leg.field)) {
1864
- throw new import_kernel10.CrossShardJoinError(
2039
+ throw new import_kernel11.CrossShardJoinError(
1865
2040
  `crossShardJoin("${leg.field}"): no ref() declared for "${leg.field}" on collection "${this.collectionName}" in template "${this.group.sharding.vaultTemplate}". Add refs: { ${leg.field}: ref('<target>') } to the template's collection options.`
1866
2041
  );
1867
2042
  }
@@ -1897,6 +2072,34 @@ var init_vault_group = __esm({
1897
2072
  }
1898
2073
  return { records: results, skippedVaults: skipped };
1899
2074
  }
2075
+ /**
2076
+ * @internal — distributed partial-reduce (#8). Fan out across eligible shards,
2077
+ * but fold each shard's where-filtered records to a partial reducer STATE
2078
+ * inside the per-shard callback (never returning the rows). Only the scalar
2079
+ * `.aggregate()` path calls this, and that path rejects join legs upstream.
2080
+ */
2081
+ async fanoutReduce(spec, options = {}) {
2082
+ const { eligible, skipped } = await this.group.resolveEligible(options);
2083
+ const across = await this.group.db.queryAcross(
2084
+ eligible.map((r) => r.vaultId),
2085
+ async (vault) => {
2086
+ this.group.template.configure(vault);
2087
+ const coll = vault.collection(this.collectionName);
2088
+ await coll.list();
2089
+ let q = coll.query();
2090
+ for (const c of this.clauses) q = q.where(c.field, c.op, c.value);
2091
+ const rows = q.toArray();
2092
+ return reduceToPartial(rows, spec);
2093
+ },
2094
+ { concurrency: options.concurrency ?? 1, create: false }
2095
+ );
2096
+ const partials = [];
2097
+ for (const r of across) {
2098
+ if (r.error) skipped.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
2099
+ else partials.push(r.result);
2100
+ }
2101
+ return { partials, skippedVaults: skipped };
2102
+ }
1900
2103
  /** Fan out across eligible shards, merge, then apply any broadcast dimension legs. */
1901
2104
  async toArray(options = {}) {
1902
2105
  const { records, skippedVaults } = await this.fanoutRecords(options);
@@ -1918,7 +2121,7 @@ var init_vault_group = __esm({
1918
2121
  /** @internal — joined queries don't support reactive/aggregate surfaces in v1. */
1919
2122
  assertNoJoinLegs(surface) {
1920
2123
  if (this.coPartitionedLegs.length || this.broadcastLegs.length) {
1921
- throw new import_kernel10.CrossShardJoinError(
2124
+ throw new import_kernel11.CrossShardJoinError(
1922
2125
  `${surface}() is not supported on a ShardedQuery with crossShardJoin/broadcastJoin legs in v1. Use toArray() for joined cross-shard queries.`
1923
2126
  );
1924
2127
  }
@@ -1984,10 +2187,10 @@ var init_vault_group = __esm({
1984
2187
  // src/index.ts
1985
2188
  var index_exports = {};
1986
2189
  __export(index_exports, {
1987
- CrossShardJoinError: () => import_kernel12.CrossShardJoinError,
2190
+ CrossShardJoinError: () => import_kernel13.CrossShardJoinError,
1988
2191
  CrossVaultDanglingRefError: () => CrossVaultDanglingRefError,
1989
2192
  CustodyApi: () => import_hub6.CustodyApi,
1990
- DataResidencyError: () => import_kernel12.DataResidencyError,
2193
+ DataResidencyError: () => import_kernel13.DataResidencyError,
1991
2194
  DockedUnit: () => DockedUnit,
1992
2195
  FieldAuthorityPolicyMissingError: () => FieldAuthorityPolicyMissingError,
1993
2196
  FieldLevelDeferredError: () => FieldLevelDeferredError,
@@ -1998,14 +2201,14 @@ __export(index_exports, {
1998
2201
  NOYDB_MULTI_BUNDLE_MAGIC: () => NOYDB_MULTI_BUNDLE_MAGIC,
1999
2202
  NOYDB_MULTI_BUNDLE_PREFIX_BYTES: () => NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
2000
2203
  NOYDB_MULTI_BUNDLE_VERSION: () => NOYDB_MULTI_BUNDLE_VERSION,
2001
- ReservedVaultNameError: () => import_kernel12.ReservedVaultNameError,
2002
- ShardProvisioningError: () => import_kernel12.ShardProvisioningError,
2204
+ ReservedVaultNameError: () => import_kernel13.ReservedVaultNameError,
2205
+ ShardProvisioningError: () => import_kernel13.ShardProvisioningError,
2003
2206
  SurfaceCadenceScheduler: () => SurfaceCadenceScheduler,
2004
2207
  SurfaceNotFoundError: () => SurfaceNotFoundError,
2005
2208
  SurfaceStateError: () => SurfaceStateError,
2006
2209
  UnitGraduationError: () => UnitGraduationError,
2007
- UnknownShardError: () => import_kernel12.UnknownShardError,
2008
- VaultTemplateNotFoundError: () => import_kernel12.VaultTemplateNotFoundError,
2210
+ UnknownShardError: () => import_kernel13.UnknownShardError,
2211
+ VaultTemplateNotFoundError: () => import_kernel13.VaultTemplateNotFoundError,
2009
2212
  agreeSurface: () => agreeSurface,
2010
2213
  applySurface: () => applySurface,
2011
2214
  createDeedOwner: () => import_hub6.createDeedOwner,
@@ -2035,7 +2238,7 @@ __export(index_exports, {
2035
2238
  writeMultiVaultBundle: () => writeMultiVaultBundle
2036
2239
  });
2037
2240
  module.exports = __toCommonJS(index_exports);
2038
- var import_kernel11 = require("@noy-db/hub/kernel");
2241
+ var import_kernel12 = require("@noy-db/hub/kernel");
2039
2242
  var import_hub5 = require("@noy-db/hub");
2040
2243
 
2041
2244
  // src/dock/docked-unit.ts
@@ -2118,7 +2321,7 @@ async function meterGroup(group, opts = {}) {
2118
2321
  }
2119
2322
 
2120
2323
  // src/index.ts
2121
- var import_kernel12 = require("@noy-db/hub/kernel");
2324
+ var import_kernel13 = require("@noy-db/hub/kernel");
2122
2325
  init_multi_bundle();
2123
2326
  init_extract_cross_vault();
2124
2327
  init_merge_compartment();
@@ -2206,15 +2409,15 @@ var Lobby = class {
2206
2409
  }
2207
2410
  async openVaultGroup(name, opts) {
2208
2411
  const db = this.noydb;
2209
- if (db.isClosed) throw new import_kernel11.ValidationError("Instance is closed");
2210
- if (name === import_hub5.STATE_VAULT_NAME) throw new import_kernel11.ReservedVaultNameError(name);
2412
+ if (db.isClosed) throw new import_kernel12.ValidationError("Instance is closed");
2413
+ if (name === import_hub5.STATE_VAULT_NAME) throw new import_kernel12.ReservedVaultNameError(name);
2211
2414
  const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
2212
- if (!template) throw new import_kernel11.VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
2415
+ if (!template) throw new import_kernel12.VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
2213
2416
  const { VaultGroup: VaultGroup2 } = await Promise.resolve().then(() => (init_vault_group(), vault_group_exports));
2214
2417
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
2215
2418
  const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(db);
2216
2419
  const registry = opts.registry ?? stateVault.registry;
2217
- const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false);
2420
+ const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false, opts.meta);
2218
2421
  if (stateVault) {
2219
2422
  group._attachStateVault(stateVault);
2220
2423
  await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
@@ -2228,7 +2431,7 @@ var Lobby = class {
2228
2431
  }
2229
2432
  async openStateManagementVault() {
2230
2433
  const db = this.noydb;
2231
- if (db.isClosed) throw new import_kernel11.ValidationError("Instance is closed");
2434
+ if (db.isClosed) throw new import_kernel12.ValidationError("Instance is closed");
2232
2435
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
2233
2436
  return StateManagementVault2.open(db);
2234
2437
  }