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

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
@@ -67,7 +67,7 @@ lobby.withVaultTemplate('client', { version: 1, configure: v => v.collection('in
67
67
  const group = await lobby.openVaultGroup('clients', { registry, sharding: { keyOf, vaultTemplate: 'client', autoCreate: true } })
68
68
  await group.shard('acme-co').collection('invoices').put('i1', { id: 'i1', total: '1200.00' })
69
69
  const all = await group.queryAcross(/* … */) // fan-out read across shards
70
- await group.migrateFleet({ batchSize: 4 }) // resumable, registry-tracked
70
+ await group.rolloutSchema({ batchSize: 4 }) // resumable, registry-tracked
71
71
  ```
72
72
 
73
73
  ### 2 · Interchange — move data between vaults, safely
@@ -137,11 +137,11 @@ await lobby.applySurface('tax-vault', surface, bundleBytes, transferKey)
137
137
 
138
138
  - **Depends on `@noy-db/hub`**, binds to the stable **`@noy-db/hub/kernel`** subpath — never reaches into hub internals.
139
139
  - **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
- - **Federation** was extracted *out of* hub into the Lobby (a breaking pre-1.0 change): `Noydb.openVaultGroup` now throws `FederationMovedError` use `lobby.openVaultGroup`.
140
+ - **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
141
  - The dependency is enforced one-way at build time; an `@noy-db` package importing `@klum-db` fails the architecture check.
142
142
 
143
143
  ## Status
144
144
 
145
- Preview, developed inside the noy-db monorepo while the kernel boundary stabilizes (it graduates to its own repo once proven). Versions track noy-db in lockstep. Pilot-1 epic (FR-1…FR-9) complete.
145
+ 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
146
 
147
- Design spec: [`docs/superpowers/specs/2026-06-16-lobby-framework-design.md`](../../docs/superpowers/specs/2026-06-16-lobby-framework-design.md). Runnable showcases: [`showcases/src/12x-klum-*`](../../showcases/src).
147
+ See [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/bin/klum.ts
21
+ var klum_exports = {};
22
+ __export(klum_exports, {
23
+ main: () => main,
24
+ parseArgs: () => parseArgs,
25
+ runInspectGroup: () => runInspectGroup,
26
+ runMeterGroup: () => runMeterGroup
27
+ });
28
+ module.exports = __toCommonJS(klum_exports);
29
+ var import_in_devtools = require("@noy-db/in-devtools");
30
+
31
+ // src/federation/group-inspector.ts
32
+ function groupInspector(group) {
33
+ let shardIds = /* @__PURE__ */ new Set();
34
+ const refresh = async () => {
35
+ const rows = await group.allRows();
36
+ shardIds = new Set(rows.map((r) => r.vaultId));
37
+ return rows;
38
+ };
39
+ return {
40
+ async listAccessibleVaults() {
41
+ const rows = await refresh();
42
+ return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
43
+ },
44
+ async openVault(name) {
45
+ const vault = await group.db.openVault(name);
46
+ group.template.configure(vault);
47
+ return vault;
48
+ },
49
+ onAfterWrite(handler) {
50
+ return group.db.onAfterWrite((event) => {
51
+ if (shardIds.has(event.vault)) return handler(event);
52
+ });
53
+ },
54
+ onWriteConflict(handler) {
55
+ return group.db.onWriteConflict((c) => {
56
+ if (shardIds.has(c.vault)) handler(c);
57
+ });
58
+ },
59
+ get writeQueue() {
60
+ return group.db.writeQueue;
61
+ }
62
+ };
63
+ }
64
+
65
+ // src/federation/meter-group.ts
66
+ async function meterGroup(group, opts = {}) {
67
+ const { eligible, skipped } = await group.resolveEligible(
68
+ opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
69
+ );
70
+ const perShard = [];
71
+ const names = /* @__PURE__ */ new Set();
72
+ let records = 0;
73
+ for (const row of eligible) {
74
+ const vault = await group.shard(row.partitionKey);
75
+ const collNames = await vault.collections();
76
+ let shardRecords = 0;
77
+ for (const n of collNames) {
78
+ names.add(n);
79
+ shardRecords += await vault.collection(n).count();
80
+ }
81
+ records += shardRecords;
82
+ perShard.push({
83
+ vaultId: row.vaultId,
84
+ partitionKey: row.partitionKey,
85
+ schemaVersion: row.schemaVersion,
86
+ collections: collNames.length,
87
+ records: shardRecords
88
+ });
89
+ }
90
+ return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
91
+ }
92
+
93
+ // src/bin/klum.ts
94
+ var import_meta = {};
95
+ function parseArgs(argv) {
96
+ const out = { command: argv[0] ?? "", meter: false };
97
+ for (const a of argv.slice(1)) {
98
+ if (a.startsWith("--group=")) out.group = a.slice("--group=".length);
99
+ else if (a.startsWith("--vault=")) out.vault = a.slice("--vault=".length);
100
+ else if (a === "--meter") out.meter = true;
101
+ else if (!a.startsWith("--")) out.configPath = a;
102
+ }
103
+ return out;
104
+ }
105
+ async function loadGroup(args) {
106
+ const mod = await import(args.configPath);
107
+ return mod.default(args.group);
108
+ }
109
+ async function runInspectGroup(args, log) {
110
+ if (!args.configPath) {
111
+ log("usage: klum inspect-group <config> --group=<name> [--vault=<id>]");
112
+ return 2;
113
+ }
114
+ const group = await loadGroup(args);
115
+ const inspector = (0, import_in_devtools.createInspector)(groupInspector(group));
116
+ const vaults = await inspector.listVaults();
117
+ log(`group "${args.group ?? ""}" \u2014 ${vaults.length} shard(s):`);
118
+ for (const v of vaults) log(` ${v.id} [${v.role}]`);
119
+ if (args.vault) {
120
+ const vault = await group.db.openVault(args.vault);
121
+ group.template.configure(vault);
122
+ const snap = await inspector.snapshot(vault);
123
+ log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(", ")}`);
124
+ }
125
+ return 0;
126
+ }
127
+ async function runMeterGroup(args, log) {
128
+ if (!args.configPath) {
129
+ log("usage: klum meter-group <config> --group=<name>");
130
+ return 2;
131
+ }
132
+ const group = await loadGroup(args);
133
+ const r = await meterGroup(group);
134
+ log(`group "${args.group ?? ""}" \u2014 ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`);
135
+ for (const s of r.perShard) {
136
+ log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`);
137
+ }
138
+ if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`);
139
+ return 0;
140
+ }
141
+ async function main(argv, log = console.log) {
142
+ const args = parseArgs(argv);
143
+ switch (args.command) {
144
+ case "inspect-group":
145
+ return runInspectGroup(args, log);
146
+ case "meter-group":
147
+ return runMeterGroup(args, log);
148
+ default:
149
+ log("klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]");
150
+ return args.command ? 1 : 0;
151
+ }
152
+ }
153
+ if (process.argv[1] && import_meta.url === `file://${process.argv[1]}`) {
154
+ main(process.argv.slice(2)).then((code) => {
155
+ process.exitCode = code;
156
+ }).catch((e) => {
157
+ console.error(e);
158
+ process.exitCode = 1;
159
+ });
160
+ }
161
+ // Annotate the CommonJS export names for ESM import in node:
162
+ 0 && (module.exports = {
163
+ main,
164
+ parseArgs,
165
+ runInspectGroup,
166
+ runMeterGroup
167
+ });
168
+ //# sourceMappingURL=klum.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/klum.ts","../../src/federation/group-inspector.ts","../../src/federation/meter-group.ts"],"sourcesContent":["import { createInspector } from '@noy-db/in-devtools'\nimport { groupInspector } from '../federation/group-inspector.js'\nimport { meterGroup } from '../federation/meter-group.js'\nimport type { VaultGroup } from '../federation/vault-group.js'\n\n/**\n * A config module for the `klum` CLI default-exports this factory: given an\n * optional group name, it returns an opened VaultGroup (the user's module owns\n * the store, templates, and sharding config — the CLI stays agnostic of them).\n */\nexport type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>\n\ntype Log = (s: string) => void\n\nexport interface ParsedArgs {\n command: string\n configPath?: string\n group?: string\n vault?: string\n meter: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = { command: argv[0] ?? '', meter: false }\n for (const a of argv.slice(1)) {\n if (a.startsWith('--group=')) out.group = a.slice('--group='.length)\n else if (a.startsWith('--vault=')) out.vault = a.slice('--vault='.length)\n else if (a === '--meter') out.meter = true\n else if (!a.startsWith('--')) out.configPath = a\n }\n return out\n}\n\nasync function loadGroup(args: ParsedArgs): Promise<VaultGroup<unknown>> {\n const mod = (await import(args.configPath!)) as { default: GroupFactory }\n return mod.default(args.group)\n}\n\nexport async function runInspectGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum inspect-group <config> --group=<name> [--vault=<id>]')\n return 2\n }\n const group = await loadGroup(args)\n const inspector = createInspector(groupInspector(group))\n const vaults = await inspector.listVaults()\n log(`group \"${args.group ?? ''}\" — ${vaults.length} shard(s):`)\n for (const v of vaults) log(` ${v.id} [${v.role}]`)\n if (args.vault) {\n const vault = await group.db.openVault(args.vault)\n group.template.configure(vault)\n const snap = await inspector.snapshot(vault)\n log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(', ')}`)\n }\n return 0\n}\n\nexport async function runMeterGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum meter-group <config> --group=<name>')\n return 2\n }\n const group = await loadGroup(args)\n const r = await meterGroup(group)\n log(`group \"${args.group ?? ''}\" — ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`)\n for (const s of r.perShard) {\n log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`)\n }\n if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`)\n return 0\n}\n\nexport async function main(argv: readonly string[], log: Log = console.log): Promise<number> {\n const args = parseArgs(argv)\n switch (args.command) {\n case 'inspect-group':\n return runInspectGroup(args, log)\n case 'meter-group':\n return runMeterGroup(args, log)\n default:\n log('klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]')\n return args.command ? 1 : 0\n }\n}\n\n// bin entrypoint — only runs when executed directly, not when imported in tests.\nif (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n console.error(e)\n process.exitCode = 1\n })\n}\n","import type { InspectableContainer } from '@noy-db/in-devtools'\nimport type { AccessibleVault, Vault, WriteHook, WriteConflict, WriteQueue, Unsubscribe } from '@noy-db/hub'\nimport type { VaultGroup } from './vault-group.js'\n\n/**\n * Adapt a federation {@link VaultGroup} to the dev-tools `InspectableContainer`\n * contract from `@noy-db/in-devtools`, so the inspector / TUI can browse a\n * whole fleet exactly like a single instance.\n *\n * Built entirely on the group's public surface (`allRows`, `db`, `template`) —\n * no `VaultGroup` changes, and (critically) no `@klum-db` import lands in any\n * `@noy-db` package: the dependency runs one way, klum → noy.\n *\n * Write-event scoping: `group.db` may host vaults outside this group, so write\n * and conflict events are filtered to the group's shard ids. The id set is\n * primed/refreshed on every `listAccessibleVaults()` call — drive `listVaults()`\n * (the inspector's normal first step) before relying on event scoping.\n */\nexport function groupInspector<T>(group: VaultGroup<T>): InspectableContainer {\n let shardIds = new Set<string>()\n const refresh = async () => {\n const rows = await group.allRows()\n shardIds = new Set(rows.map((r) => r.vaultId))\n return rows\n }\n return {\n async listAccessibleVaults(): Promise<readonly AccessibleVault[]> {\n const rows = await refresh()\n return rows.map((r): AccessibleVault => ({ id: r.vaultId, role: 'owner' }))\n },\n async openVault(name: string): Promise<Vault> {\n const vault = await group.db.openVault(name)\n group.template.configure(vault)\n return vault\n },\n onAfterWrite(handler: WriteHook): Unsubscribe {\n return group.db.onAfterWrite((event) => {\n if (shardIds.has(event.vault)) return handler(event)\n })\n },\n onWriteConflict(handler: (c: WriteConflict) => void): Unsubscribe {\n return group.db.onWriteConflict((c) => {\n if (shardIds.has(c.vault)) handler(c)\n })\n },\n get writeQueue(): WriteQueue {\n return group.db.writeQueue\n },\n }\n}\n","import type { VaultGroup } from './vault-group.js'\nimport type { SkippedVault } from './types.js'\n\nexport interface GroupShardMetrics {\n readonly vaultId: string\n readonly partitionKey: string\n readonly schemaVersion: number\n readonly collections: number\n readonly records: number\n}\n\nexport interface GroupMeterReport {\n /** Number of eligible shards measured. */\n readonly vaults: number\n /** Distinct collection names across the group. */\n readonly collections: number\n /** Total record count summed across shards. */\n readonly records: number\n readonly perShard: ReadonlyArray<GroupShardMetrics>\n /** Drifted / provisioning-failed shards — surfaced, never counted or hidden. */\n readonly skipped: ReadonlyArray<SkippedVault>\n}\n\n/**\n * Fan shape-metrics (collection count + record count) across the group's\n * ELIGIBLE shards. Skipped shards (schema-drift / provisioning failures) are\n * reported in `skipped`, never silently dropped. Reuses the per-vault pattern\n * from `multi-bundle.ts` (`vault.collections()` → `collection(n).count()`).\n *\n * Operational store metrics (calls, CAS conflicts) are a separate concern and\n * are already group-wide via `@noy-db/to-meter` on the underlying store.\n */\nexport async function meterGroup<T>(\n group: VaultGroup<T>,\n opts: { minVersion?: number } = {},\n): Promise<GroupMeterReport> {\n const { eligible, skipped } = await group.resolveEligible(\n opts.minVersion !== undefined ? { minVersion: opts.minVersion } : {},\n )\n const perShard: GroupShardMetrics[] = []\n const names = new Set<string>()\n let records = 0\n for (const row of eligible) {\n const vault = await group.shard(row.partitionKey)\n const collNames = await vault.collections()\n let shardRecords = 0\n for (const n of collNames) {\n names.add(n)\n shardRecords += await vault.collection(n).count()\n }\n records += shardRecords\n perShard.push({\n vaultId: row.vaultId,\n partitionKey: row.partitionKey,\n schemaVersion: row.schemaVersion,\n collections: collNames.length,\n records: shardRecords,\n })\n }\n return { vaults: eligible.length, collections: names.size, records, perShard, skipped }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,25 @@
1
+ import { V as VaultGroup } from '../vault-group-BXjO5kHB.cjs';
2
+ import '@noy-db/hub/kernel';
3
+ import '@noy-db/hub';
4
+ import '@noy-db/hub/bundle';
5
+
6
+ /**
7
+ * A config module for the `klum` CLI default-exports this factory: given an
8
+ * optional group name, it returns an opened VaultGroup (the user's module owns
9
+ * the store, templates, and sharding config — the CLI stays agnostic of them).
10
+ */
11
+ type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>;
12
+ type Log = (s: string) => void;
13
+ interface ParsedArgs {
14
+ command: string;
15
+ configPath?: string;
16
+ group?: string;
17
+ vault?: string;
18
+ meter: boolean;
19
+ }
20
+ declare function parseArgs(argv: readonly string[]): ParsedArgs;
21
+ declare function runInspectGroup(args: ParsedArgs, log: Log): Promise<number>;
22
+ declare function runMeterGroup(args: ParsedArgs, log: Log): Promise<number>;
23
+ declare function main(argv: readonly string[], log?: Log): Promise<number>;
24
+
25
+ export { type GroupFactory, type ParsedArgs, main, parseArgs, runInspectGroup, runMeterGroup };
@@ -0,0 +1,25 @@
1
+ import { V as VaultGroup } from '../vault-group-BXjO5kHB.js';
2
+ import '@noy-db/hub/kernel';
3
+ import '@noy-db/hub';
4
+ import '@noy-db/hub/bundle';
5
+
6
+ /**
7
+ * A config module for the `klum` CLI default-exports this factory: given an
8
+ * optional group name, it returns an opened VaultGroup (the user's module owns
9
+ * the store, templates, and sharding config — the CLI stays agnostic of them).
10
+ */
11
+ type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>;
12
+ type Log = (s: string) => void;
13
+ interface ParsedArgs {
14
+ command: string;
15
+ configPath?: string;
16
+ group?: string;
17
+ vault?: string;
18
+ meter: boolean;
19
+ }
20
+ declare function parseArgs(argv: readonly string[]): ParsedArgs;
21
+ declare function runInspectGroup(args: ParsedArgs, log: Log): Promise<number>;
22
+ declare function runMeterGroup(args: ParsedArgs, log: Log): Promise<number>;
23
+ declare function main(argv: readonly string[], log?: Log): Promise<number>;
24
+
25
+ export { type GroupFactory, type ParsedArgs, main, parseArgs, runInspectGroup, runMeterGroup };
@@ -0,0 +1,139 @@
1
+ // src/bin/klum.ts
2
+ import { createInspector } from "@noy-db/in-devtools";
3
+
4
+ // src/federation/group-inspector.ts
5
+ function groupInspector(group) {
6
+ let shardIds = /* @__PURE__ */ new Set();
7
+ const refresh = async () => {
8
+ const rows = await group.allRows();
9
+ shardIds = new Set(rows.map((r) => r.vaultId));
10
+ return rows;
11
+ };
12
+ return {
13
+ async listAccessibleVaults() {
14
+ const rows = await refresh();
15
+ return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
16
+ },
17
+ async openVault(name) {
18
+ const vault = await group.db.openVault(name);
19
+ group.template.configure(vault);
20
+ return vault;
21
+ },
22
+ onAfterWrite(handler) {
23
+ return group.db.onAfterWrite((event) => {
24
+ if (shardIds.has(event.vault)) return handler(event);
25
+ });
26
+ },
27
+ onWriteConflict(handler) {
28
+ return group.db.onWriteConflict((c) => {
29
+ if (shardIds.has(c.vault)) handler(c);
30
+ });
31
+ },
32
+ get writeQueue() {
33
+ return group.db.writeQueue;
34
+ }
35
+ };
36
+ }
37
+
38
+ // src/federation/meter-group.ts
39
+ async function meterGroup(group, opts = {}) {
40
+ const { eligible, skipped } = await group.resolveEligible(
41
+ opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
42
+ );
43
+ const perShard = [];
44
+ const names = /* @__PURE__ */ new Set();
45
+ let records = 0;
46
+ for (const row of eligible) {
47
+ const vault = await group.shard(row.partitionKey);
48
+ const collNames = await vault.collections();
49
+ let shardRecords = 0;
50
+ for (const n of collNames) {
51
+ names.add(n);
52
+ shardRecords += await vault.collection(n).count();
53
+ }
54
+ records += shardRecords;
55
+ perShard.push({
56
+ vaultId: row.vaultId,
57
+ partitionKey: row.partitionKey,
58
+ schemaVersion: row.schemaVersion,
59
+ collections: collNames.length,
60
+ records: shardRecords
61
+ });
62
+ }
63
+ return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
64
+ }
65
+
66
+ // src/bin/klum.ts
67
+ function parseArgs(argv) {
68
+ const out = { command: argv[0] ?? "", meter: false };
69
+ for (const a of argv.slice(1)) {
70
+ if (a.startsWith("--group=")) out.group = a.slice("--group=".length);
71
+ else if (a.startsWith("--vault=")) out.vault = a.slice("--vault=".length);
72
+ else if (a === "--meter") out.meter = true;
73
+ else if (!a.startsWith("--")) out.configPath = a;
74
+ }
75
+ return out;
76
+ }
77
+ async function loadGroup(args) {
78
+ const mod = await import(args.configPath);
79
+ return mod.default(args.group);
80
+ }
81
+ async function runInspectGroup(args, log) {
82
+ if (!args.configPath) {
83
+ log("usage: klum inspect-group <config> --group=<name> [--vault=<id>]");
84
+ return 2;
85
+ }
86
+ const group = await loadGroup(args);
87
+ const inspector = createInspector(groupInspector(group));
88
+ const vaults = await inspector.listVaults();
89
+ log(`group "${args.group ?? ""}" \u2014 ${vaults.length} shard(s):`);
90
+ for (const v of vaults) log(` ${v.id} [${v.role}]`);
91
+ if (args.vault) {
92
+ const vault = await group.db.openVault(args.vault);
93
+ group.template.configure(vault);
94
+ const snap = await inspector.snapshot(vault);
95
+ log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(", ")}`);
96
+ }
97
+ return 0;
98
+ }
99
+ async function runMeterGroup(args, log) {
100
+ if (!args.configPath) {
101
+ log("usage: klum meter-group <config> --group=<name>");
102
+ return 2;
103
+ }
104
+ const group = await loadGroup(args);
105
+ const r = await meterGroup(group);
106
+ log(`group "${args.group ?? ""}" \u2014 ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`);
107
+ for (const s of r.perShard) {
108
+ log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`);
109
+ }
110
+ if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`);
111
+ return 0;
112
+ }
113
+ async function main(argv, log = console.log) {
114
+ const args = parseArgs(argv);
115
+ switch (args.command) {
116
+ case "inspect-group":
117
+ return runInspectGroup(args, log);
118
+ case "meter-group":
119
+ return runMeterGroup(args, log);
120
+ default:
121
+ log("klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]");
122
+ return args.command ? 1 : 0;
123
+ }
124
+ }
125
+ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
126
+ main(process.argv.slice(2)).then((code) => {
127
+ process.exitCode = code;
128
+ }).catch((e) => {
129
+ console.error(e);
130
+ process.exitCode = 1;
131
+ });
132
+ }
133
+ export {
134
+ main,
135
+ parseArgs,
136
+ runInspectGroup,
137
+ runMeterGroup
138
+ };
139
+ //# sourceMappingURL=klum.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/klum.ts","../../src/federation/group-inspector.ts","../../src/federation/meter-group.ts"],"sourcesContent":["import { createInspector } from '@noy-db/in-devtools'\nimport { groupInspector } from '../federation/group-inspector.js'\nimport { meterGroup } from '../federation/meter-group.js'\nimport type { VaultGroup } from '../federation/vault-group.js'\n\n/**\n * A config module for the `klum` CLI default-exports this factory: given an\n * optional group name, it returns an opened VaultGroup (the user's module owns\n * the store, templates, and sharding config — the CLI stays agnostic of them).\n */\nexport type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>\n\ntype Log = (s: string) => void\n\nexport interface ParsedArgs {\n command: string\n configPath?: string\n group?: string\n vault?: string\n meter: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = { command: argv[0] ?? '', meter: false }\n for (const a of argv.slice(1)) {\n if (a.startsWith('--group=')) out.group = a.slice('--group='.length)\n else if (a.startsWith('--vault=')) out.vault = a.slice('--vault='.length)\n else if (a === '--meter') out.meter = true\n else if (!a.startsWith('--')) out.configPath = a\n }\n return out\n}\n\nasync function loadGroup(args: ParsedArgs): Promise<VaultGroup<unknown>> {\n const mod = (await import(args.configPath!)) as { default: GroupFactory }\n return mod.default(args.group)\n}\n\nexport async function runInspectGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum inspect-group <config> --group=<name> [--vault=<id>]')\n return 2\n }\n const group = await loadGroup(args)\n const inspector = createInspector(groupInspector(group))\n const vaults = await inspector.listVaults()\n log(`group \"${args.group ?? ''}\" — ${vaults.length} shard(s):`)\n for (const v of vaults) log(` ${v.id} [${v.role}]`)\n if (args.vault) {\n const vault = await group.db.openVault(args.vault)\n group.template.configure(vault)\n const snap = await inspector.snapshot(vault)\n log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(', ')}`)\n }\n return 0\n}\n\nexport async function runMeterGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum meter-group <config> --group=<name>')\n return 2\n }\n const group = await loadGroup(args)\n const r = await meterGroup(group)\n log(`group \"${args.group ?? ''}\" — ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`)\n for (const s of r.perShard) {\n log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`)\n }\n if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`)\n return 0\n}\n\nexport async function main(argv: readonly string[], log: Log = console.log): Promise<number> {\n const args = parseArgs(argv)\n switch (args.command) {\n case 'inspect-group':\n return runInspectGroup(args, log)\n case 'meter-group':\n return runMeterGroup(args, log)\n default:\n log('klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]')\n return args.command ? 1 : 0\n }\n}\n\n// bin entrypoint — only runs when executed directly, not when imported in tests.\nif (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n console.error(e)\n process.exitCode = 1\n })\n}\n","import type { InspectableContainer } from '@noy-db/in-devtools'\nimport type { AccessibleVault, Vault, WriteHook, WriteConflict, WriteQueue, Unsubscribe } from '@noy-db/hub'\nimport type { VaultGroup } from './vault-group.js'\n\n/**\n * Adapt a federation {@link VaultGroup} to the dev-tools `InspectableContainer`\n * contract from `@noy-db/in-devtools`, so the inspector / TUI can browse a\n * whole fleet exactly like a single instance.\n *\n * Built entirely on the group's public surface (`allRows`, `db`, `template`) —\n * no `VaultGroup` changes, and (critically) no `@klum-db` import lands in any\n * `@noy-db` package: the dependency runs one way, klum → noy.\n *\n * Write-event scoping: `group.db` may host vaults outside this group, so write\n * and conflict events are filtered to the group's shard ids. The id set is\n * primed/refreshed on every `listAccessibleVaults()` call — drive `listVaults()`\n * (the inspector's normal first step) before relying on event scoping.\n */\nexport function groupInspector<T>(group: VaultGroup<T>): InspectableContainer {\n let shardIds = new Set<string>()\n const refresh = async () => {\n const rows = await group.allRows()\n shardIds = new Set(rows.map((r) => r.vaultId))\n return rows\n }\n return {\n async listAccessibleVaults(): Promise<readonly AccessibleVault[]> {\n const rows = await refresh()\n return rows.map((r): AccessibleVault => ({ id: r.vaultId, role: 'owner' }))\n },\n async openVault(name: string): Promise<Vault> {\n const vault = await group.db.openVault(name)\n group.template.configure(vault)\n return vault\n },\n onAfterWrite(handler: WriteHook): Unsubscribe {\n return group.db.onAfterWrite((event) => {\n if (shardIds.has(event.vault)) return handler(event)\n })\n },\n onWriteConflict(handler: (c: WriteConflict) => void): Unsubscribe {\n return group.db.onWriteConflict((c) => {\n if (shardIds.has(c.vault)) handler(c)\n })\n },\n get writeQueue(): WriteQueue {\n return group.db.writeQueue\n },\n }\n}\n","import type { VaultGroup } from './vault-group.js'\nimport type { SkippedVault } from './types.js'\n\nexport interface GroupShardMetrics {\n readonly vaultId: string\n readonly partitionKey: string\n readonly schemaVersion: number\n readonly collections: number\n readonly records: number\n}\n\nexport interface GroupMeterReport {\n /** Number of eligible shards measured. */\n readonly vaults: number\n /** Distinct collection names across the group. */\n readonly collections: number\n /** Total record count summed across shards. */\n readonly records: number\n readonly perShard: ReadonlyArray<GroupShardMetrics>\n /** Drifted / provisioning-failed shards — surfaced, never counted or hidden. */\n readonly skipped: ReadonlyArray<SkippedVault>\n}\n\n/**\n * Fan shape-metrics (collection count + record count) across the group's\n * ELIGIBLE shards. Skipped shards (schema-drift / provisioning failures) are\n * reported in `skipped`, never silently dropped. Reuses the per-vault pattern\n * from `multi-bundle.ts` (`vault.collections()` → `collection(n).count()`).\n *\n * Operational store metrics (calls, CAS conflicts) are a separate concern and\n * are already group-wide via `@noy-db/to-meter` on the underlying store.\n */\nexport async function meterGroup<T>(\n group: VaultGroup<T>,\n opts: { minVersion?: number } = {},\n): Promise<GroupMeterReport> {\n const { eligible, skipped } = await group.resolveEligible(\n opts.minVersion !== undefined ? { minVersion: opts.minVersion } : {},\n )\n const perShard: GroupShardMetrics[] = []\n const names = new Set<string>()\n let records = 0\n for (const row of eligible) {\n const vault = await group.shard(row.partitionKey)\n const collNames = await vault.collections()\n let shardRecords = 0\n for (const n of collNames) {\n names.add(n)\n shardRecords += await vault.collection(n).count()\n }\n records += shardRecords\n perShard.push({\n vaultId: row.vaultId,\n partitionKey: row.partitionKey,\n schemaVersion: row.schemaVersion,\n collections: collNames.length,\n records: shardRecords,\n })\n }\n return { vaults: eligible.length, collections: names.size, records, perShard, skipped }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -1415,13 +1415,13 @@ var init_vault_group = __esm({
1415
1415
  SHARD_SEPARATOR = "--";
1416
1416
  SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
1417
1417
  VaultGroup = class {
1418
- constructor(db, name, registry, sharding, template, migrateOnOpen = false) {
1418
+ constructor(db, name, registry, sharding, template, cutoverOnOpen = false) {
1419
1419
  this.db = db;
1420
1420
  this.name = name;
1421
1421
  this.registry = registry;
1422
1422
  this.sharding = sharding;
1423
1423
  this.template = template;
1424
- this.migrateOnOpen = migrateOnOpen;
1424
+ this.cutoverOnOpen = cutoverOnOpen;
1425
1425
  if (name.includes(SHARD_SEPARATOR)) {
1426
1426
  throw new import_kernel10.ValidationError(
1427
1427
  `VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
@@ -1433,7 +1433,7 @@ var init_vault_group = __esm({
1433
1433
  registry;
1434
1434
  sharding;
1435
1435
  template;
1436
- migrateOnOpen;
1436
+ cutoverOnOpen;
1437
1437
  /** @internal — set when the group is managed (no explicit registry). */
1438
1438
  stateVault;
1439
1439
  /** @internal */
@@ -1468,20 +1468,20 @@ var init_vault_group = __esm({
1468
1468
  return rows.filter((r) => r.group === this.name);
1469
1469
  }
1470
1470
  /**
1471
- * Open an existing shard and apply the template. When `migrateOnOpen` is set
1471
+ * Open an existing shard and apply the template. When `cutoverOnOpen` is set
1472
1472
  * (#271) and the shard's registry version is behind the template, its cutover
1473
1473
  * runs inline first — so a behind shard never surfaces a stale handle.
1474
1474
  */
1475
1475
  async openShard(partitionKey) {
1476
- if (this.migrateOnOpen) {
1476
+ if (this.cutoverOnOpen) {
1477
1477
  const row = await this.registry.get(this.registryId(partitionKey));
1478
1478
  if (row && row.schemaVersion < this.template.version) {
1479
- await this.migrateShard(partitionKey);
1479
+ await this.cutoverShard(partitionKey);
1480
1480
  }
1481
1481
  }
1482
1482
  return this._openShardRaw(partitionKey);
1483
1483
  }
1484
- /** @internal — open + configure with no migrate-on-open hook (used by the migration path itself to avoid recursion). */
1484
+ /** @internal — open + configure with no cutover-on-open hook (used by the migration path itself to avoid recursion). */
1485
1485
  async _openShardRaw(partitionKey) {
1486
1486
  const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
1487
1487
  this.template.configure(vault);
@@ -1703,7 +1703,7 @@ var init_vault_group = __esm({
1703
1703
  * Never throws on a cutover failure — it records `status: 'failed'` and
1704
1704
  * returns the row, so a fleet run continues past a bad shard.
1705
1705
  */
1706
- async migrateShard(partitionKey) {
1706
+ async cutoverShard(partitionKey) {
1707
1707
  const vaultId = this.shardVaultId(partitionKey);
1708
1708
  const row = await this.registry.get(this.registryId(partitionKey));
1709
1709
  if (!row) throw new import_kernel10.UnknownShardError(partitionKey, this.name);
@@ -1754,7 +1754,7 @@ var init_vault_group = __esm({
1754
1754
  * - `batchSize` — max shards migrated concurrently per batch (back-pressure).
1755
1755
  * Default 4. Batches run sequentially; shards within a batch run in parallel.
1756
1756
  */
1757
- async migrateFleet(options = {}) {
1757
+ async rolloutSchema(options = {}) {
1758
1758
  const target = this.template.version;
1759
1759
  const rows = await this.allRows();
1760
1760
  const cohort = options.cohort;
@@ -1766,7 +1766,7 @@ var init_vault_group = __esm({
1766
1766
  const failed = [];
1767
1767
  for (let i = 0; i < todo.length; i += batchSize) {
1768
1768
  const batch = todo.slice(i, i + batchSize);
1769
- const settled = await Promise.all(batch.map((r) => this.migrateShard(r.partitionKey)));
1769
+ const settled = await Promise.all(batch.map((r) => this.cutoverShard(r.partitionKey)));
1770
1770
  for (const res of settled) {
1771
1771
  if (res.status === "done") migrated.push(res.vaultId);
1772
1772
  else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
@@ -2015,6 +2015,7 @@ __export(index_exports, {
2015
2015
  encodeMultiBundle: () => encodeMultiBundle,
2016
2016
  exportSurface: () => exportSurface,
2017
2017
  extractCrossVaultPartition: () => extractCrossVaultPartition,
2018
+ groupInspector: () => groupInspector,
2018
2019
  isDeedVault: () => import_hub6.isDeedVault,
2019
2020
  isSurfaceDue: () => isSurfaceDue,
2020
2021
  liberateVault: () => import_hub6.liberateVault,
@@ -2023,6 +2024,7 @@ __export(index_exports, {
2023
2024
  markSynced: () => markSynced,
2024
2025
  mergeCompartment: () => mergeCompartment,
2025
2026
  mergeDecryptedRecords: () => mergeDecryptedRecords,
2027
+ meterGroup: () => meterGroup,
2026
2028
  migrateThenMerge: () => migrateThenMerge,
2027
2029
  proposeSurface: () => proposeSurface,
2028
2030
  readMultiVaultBundleCompartment: () => readMultiVaultBundleCompartment,
@@ -2053,6 +2055,68 @@ var DockedUnit = class {
2053
2055
  }
2054
2056
  };
2055
2057
 
2058
+ // src/federation/group-inspector.ts
2059
+ function groupInspector(group) {
2060
+ let shardIds = /* @__PURE__ */ new Set();
2061
+ const refresh = async () => {
2062
+ const rows = await group.allRows();
2063
+ shardIds = new Set(rows.map((r) => r.vaultId));
2064
+ return rows;
2065
+ };
2066
+ return {
2067
+ async listAccessibleVaults() {
2068
+ const rows = await refresh();
2069
+ return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
2070
+ },
2071
+ async openVault(name) {
2072
+ const vault = await group.db.openVault(name);
2073
+ group.template.configure(vault);
2074
+ return vault;
2075
+ },
2076
+ onAfterWrite(handler) {
2077
+ return group.db.onAfterWrite((event) => {
2078
+ if (shardIds.has(event.vault)) return handler(event);
2079
+ });
2080
+ },
2081
+ onWriteConflict(handler) {
2082
+ return group.db.onWriteConflict((c) => {
2083
+ if (shardIds.has(c.vault)) handler(c);
2084
+ });
2085
+ },
2086
+ get writeQueue() {
2087
+ return group.db.writeQueue;
2088
+ }
2089
+ };
2090
+ }
2091
+
2092
+ // src/federation/meter-group.ts
2093
+ async function meterGroup(group, opts = {}) {
2094
+ const { eligible, skipped } = await group.resolveEligible(
2095
+ opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
2096
+ );
2097
+ const perShard = [];
2098
+ const names = /* @__PURE__ */ new Set();
2099
+ let records = 0;
2100
+ for (const row of eligible) {
2101
+ const vault = await group.shard(row.partitionKey);
2102
+ const collNames = await vault.collections();
2103
+ let shardRecords = 0;
2104
+ for (const n of collNames) {
2105
+ names.add(n);
2106
+ shardRecords += await vault.collection(n).count();
2107
+ }
2108
+ records += shardRecords;
2109
+ perShard.push({
2110
+ vaultId: row.vaultId,
2111
+ partitionKey: row.partitionKey,
2112
+ schemaVersion: row.schemaVersion,
2113
+ collections: collNames.length,
2114
+ records: shardRecords
2115
+ });
2116
+ }
2117
+ return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
2118
+ }
2119
+
2056
2120
  // src/index.ts
2057
2121
  var import_kernel12 = require("@noy-db/hub/kernel");
2058
2122
  init_multi_bundle();
@@ -2150,7 +2214,7 @@ var Lobby = class {
2150
2214
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
2151
2215
  const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(db);
2152
2216
  const registry = opts.registry ?? stateVault.registry;
2153
- const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
2217
+ const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false);
2154
2218
  if (stateVault) {
2155
2219
  group._attachStateVault(stateVault);
2156
2220
  await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
@@ -2283,6 +2347,7 @@ function createLobby(noydb) {
2283
2347
  encodeMultiBundle,
2284
2348
  exportSurface,
2285
2349
  extractCrossVaultPartition,
2350
+ groupInspector,
2286
2351
  isDeedVault,
2287
2352
  isSurfaceDue,
2288
2353
  liberateVault,
@@ -2291,6 +2356,7 @@ function createLobby(noydb) {
2291
2356
  markSynced,
2292
2357
  mergeCompartment,
2293
2358
  mergeDecryptedRecords,
2359
+ meterGroup,
2294
2360
  migrateThenMerge,
2295
2361
  proposeSurface,
2296
2362
  readMultiVaultBundleCompartment,