@noy-db/hub 0.6.0-pre.4 → 0.6.0-pre.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/codemods/0.4.0-pre.json +40 -0
  2. package/dist/broker/index.js +3 -3
  3. package/dist/cargo/index.js +4 -4
  4. package/dist/{chunk-AHCSWNVL.js → chunk-4R55OPT2.js} +3 -3
  5. package/dist/{chunk-XMC2ZTRD.js → chunk-ADW22QSB.js} +3 -3
  6. package/dist/chunk-GOLN6LQ5.js +123 -0
  7. package/dist/chunk-GOLN6LQ5.js.map +1 -0
  8. package/dist/{chunk-RW3XKAF3.js → chunk-JRKZTEZZ.js} +60 -4
  9. package/dist/chunk-JRKZTEZZ.js.map +1 -0
  10. package/dist/{chunk-MZ5ZFO2D.js → chunk-NNYENE7N.js} +50 -112
  11. package/dist/chunk-NNYENE7N.js.map +1 -0
  12. package/dist/{chunk-WDDHMAAO.js → chunk-NPP5R7FF.js} +2 -2
  13. package/dist/{chunk-O42AUQCP.js → chunk-O2ERS4UJ.js} +2 -2
  14. package/dist/{chunk-MYG6WIRJ.js → chunk-PXCBVDFY.js} +49 -12
  15. package/dist/chunk-PXCBVDFY.js.map +1 -0
  16. package/dist/{chunk-LWSJG3LI.js → chunk-RTAMLDWP.js} +6 -2
  17. package/dist/chunk-RTAMLDWP.js.map +1 -0
  18. package/dist/{chunk-HY6Y2P4Y.js → chunk-WNFZI3I5.js} +3 -3
  19. package/dist/{chunk-VZUVORWV.js → chunk-XBRNBG4Z.js} +2 -2
  20. package/dist/{chunk-IQRP74OO.js → chunk-YJ6IONGA.js} +2 -2
  21. package/dist/{chunk-7V2OGKES.js → chunk-YM75S6N6.js} +112 -61
  22. package/dist/chunk-YM75S6N6.js.map +1 -0
  23. package/dist/{chunk-Q2VEFHDQ.js → chunk-ZMSE2ZZ4.js} +26 -1
  24. package/dist/chunk-ZMSE2ZZ4.js.map +1 -0
  25. package/dist/custody/index.js +2 -2
  26. package/dist/{dispatch-5DN7ZJGM.js → dispatch-4NSDG6EP.js} +5 -5
  27. package/dist/{executor-TZNM4GCW.js → executor-HYQBNG6J.js} +3 -2
  28. package/dist/guards/index.js +1 -1
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +24 -20
  31. package/dist/index.js.map +1 -1
  32. package/dist/kernel/vault.d.ts +9 -11
  33. package/dist/{liberate-BAHOP2TQ.js → liberate-GFN4C7RR.js} +3 -3
  34. package/dist/materialized-views/index.js +3 -2
  35. package/dist/money/index.js +10 -6
  36. package/dist/{noydb-QGPKEDXK.js → noydb-LYIAD4CW.js} +6 -6
  37. package/dist/periods/index.js +5 -1
  38. package/dist/pod/index.js +6 -6
  39. package/dist/{seed-NPF3BZGQ.js → seed-GDV5PT4R.js} +2 -2
  40. package/dist/{stale-4E3VZXGV.js → stale-C27ICSB4.js} +2 -2
  41. package/dist/team/index.js +3 -3
  42. package/dist/via/money/exact.d.ts +44 -0
  43. package/dist/via/money/index.d.ts +2 -0
  44. package/dist/with-audit/guards/immutable-guard.d.ts +14 -0
  45. package/dist/with-audit/periods/active.d.ts +28 -1
  46. package/dist/with-audit/periods/index.d.ts +4 -2
  47. package/dist/with-audit/periods/periods.d.ts +79 -4
  48. package/dist/with-audit/periods/strategy.d.ts +13 -4
  49. package/dist/with-audit/periods/vault-facade.d.ts +36 -10
  50. package/dist/with-formula/materialized-views/types.d.ts +52 -0
  51. package/dist/with-party/team/keyring.d.ts +10 -1
  52. package/package.json +3 -3
  53. package/dist/chunk-7V2OGKES.js.map +0 -1
  54. package/dist/chunk-LWSJG3LI.js.map +0 -1
  55. package/dist/chunk-MYG6WIRJ.js.map +0 -1
  56. package/dist/chunk-MZ5ZFO2D.js.map +0 -1
  57. package/dist/chunk-Q2VEFHDQ.js.map +0 -1
  58. package/dist/chunk-RW3XKAF3.js.map +0 -1
  59. /package/dist/{chunk-AHCSWNVL.js.map → chunk-4R55OPT2.js.map} +0 -0
  60. /package/dist/{chunk-XMC2ZTRD.js.map → chunk-ADW22QSB.js.map} +0 -0
  61. /package/dist/{chunk-WDDHMAAO.js.map → chunk-NPP5R7FF.js.map} +0 -0
  62. /package/dist/{chunk-O42AUQCP.js.map → chunk-O2ERS4UJ.js.map} +0 -0
  63. /package/dist/{chunk-HY6Y2P4Y.js.map → chunk-WNFZI3I5.js.map} +0 -0
  64. /package/dist/{chunk-VZUVORWV.js.map → chunk-XBRNBG4Z.js.map} +0 -0
  65. /package/dist/{chunk-IQRP74OO.js.map → chunk-YJ6IONGA.js.map} +0 -0
  66. /package/dist/{dispatch-5DN7ZJGM.js.map → dispatch-4NSDG6EP.js.map} +0 -0
  67. /package/dist/{executor-TZNM4GCW.js.map → executor-HYQBNG6J.js.map} +0 -0
  68. /package/dist/{liberate-BAHOP2TQ.js.map → liberate-GFN4C7RR.js.map} +0 -0
  69. /package/dist/{noydb-QGPKEDXK.js.map → noydb-LYIAD4CW.js.map} +0 -0
  70. /package/dist/{seed-NPF3BZGQ.js.map → seed-GDV5PT4R.js.map} +0 -0
  71. /package/dist/{stale-4E3VZXGV.js.map → stale-C27ICSB4.js.map} +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Exact decimal arithmetic for post-aggregate row math (#1007).
3
+ *
4
+ * Money leaves a reducer as a `MoneyString` — a decimal string, exact by
5
+ * construction. Doing `Number(a) - Number(b)` on two of those re-introduces
6
+ * binary floating point at the one step the money type exists to protect:
7
+ * `10.05 - 0.10` becomes `9.950000000000001`, which `quantizeMoneyFields`
8
+ * then correctly REFUSES rather than silently storing drift.
9
+ *
10
+ * So `derive` is handed these instead. Everything runs in scaled BigInt at the
11
+ * widest scale of its inputs and formats back to a decimal string, which means
12
+ * no operation here can introduce a representation error. Deliberately just
13
+ * the additive set — `add` / `sub` / `neg` / `min` / `max` / `cmp`. Anything
14
+ * needing multiplication or division needs a rounding policy, and a rounding
15
+ * policy is a decision the caller must make explicitly, not one this module
16
+ * should guess.
17
+ *
18
+ * @module
19
+ */
20
+ /** A value these helpers accept: a decimal string, a number, or a bigint of whole units. */
21
+ export type ExactOperand = string | number | bigint;
22
+ /**
23
+ * Exact decimal helpers handed to an MV `derive` as its second argument.
24
+ *
25
+ * Every result is a decimal string, so results compose without ever passing
26
+ * through a float. Declare the derived field in the MV's `moneyFields` and the
27
+ * final value is quantised to that descriptor's scale on the way to storage.
28
+ */
29
+ export interface ExactMath {
30
+ /** `a + b`, exact. */
31
+ add(a: ExactOperand, b: ExactOperand): string;
32
+ /** `a - b`, exact. */
33
+ sub(a: ExactOperand, b: ExactOperand): string;
34
+ /** `-a`, exact. */
35
+ neg(a: ExactOperand): string;
36
+ /** The larger of `a` and `b`. */
37
+ max(a: ExactOperand, b: ExactOperand): string;
38
+ /** The smaller of `a` and `b`. */
39
+ min(a: ExactOperand, b: ExactOperand): string;
40
+ /** `-1` when `a < b`, `0` when equal, `1` when `a > b`. */
41
+ cmp(a: ExactOperand, b: ExactOperand): -1 | 0 | 1;
42
+ }
43
+ /** The singleton passed to `derive` — stateless, so there is nothing to construct per row. */
44
+ export declare const exactMath: ExactMath;
@@ -13,3 +13,5 @@ export { mulRate, allocate } from './arith.js';
13
13
  export type { MulRateOptions, AllocateOptions } from './arith.js';
14
14
  export { asMoney, isMoneyString, moneyNumber } from './branded.js';
15
15
  export type { MoneyString } from './branded.js';
16
+ export { exactMath } from './exact.js';
17
+ export type { ExactMath, ExactOperand } from './exact.js';
@@ -29,6 +29,20 @@ import type { GuardStrategy, GuardContext, GuardChange } from './types.js';
29
29
  export interface ImmutableGuardConfig<T extends Record<string, unknown>> {
30
30
  /** The collection to make WORM. */
31
31
  collection: string;
32
+ /**
33
+ * Optional stable per-vault identifier, forwarded verbatim to the
34
+ * underlying {@link GuardSpec.name} — `immutableGuard` is a wrapper
35
+ * around `withGuard` and grants the same identity affordance (#1006).
36
+ *
37
+ * Omit and `vault.listBehaviors()` falls back to a POSITIONAL
38
+ * `${collection}#${occurrence}` key, which renumbers when another
39
+ * guard on the same collection is registered ahead of this one. Pass
40
+ * a name whenever something joins to the behavior manifest by key —
41
+ * a generated rulebook, a diff between two vault versions, an audit
42
+ * report — so the identifier tracks the rule rather than its
43
+ * registration order.
44
+ */
45
+ readonly name?: string;
32
46
  /**
33
47
  * A record becomes immutable once this predicate holds. Evaluated on
34
48
  * the *existing* (already-persisted) record, so the write that first
@@ -2,10 +2,37 @@
2
2
  * Active periods strategy factory. Only reachable through the
3
3
  * `@noy-db/hub/periods` subpath.
4
4
  */
5
+ import type { PeriodPartition } from './periods.js';
5
6
  import type { PeriodsStrategy } from './strategy.js';
7
+ /** Options for {@link withPeriods}. */
8
+ export interface WithPeriodsOptions {
9
+ /**
10
+ * Maps a collection to the timeline each of its records belongs to (#1005) —
11
+ * the answer to "which close calendar governs THIS record".
12
+ *
13
+ * ```ts
14
+ * withPeriods({
15
+ * subjects: { receipts: (r) => [r.clientId, layerOf(r)] },
16
+ * })
17
+ * ```
18
+ *
19
+ * Same shape as `withForget({ subjects })`, which answers the same question
20
+ * for erasure. A collection with no entry — and every collection when
21
+ * `subjects` is omitted entirely — stays on the vault-wide timeline, so an
22
+ * existing vault behaves exactly as it did before partitions existed.
23
+ *
24
+ * Return `undefined` from a mapper to put an individual record back on the
25
+ * vault-wide timeline (e.g. a record that predates the field the mapping
26
+ * reads).
27
+ */
28
+ readonly subjects?: Readonly<Record<string, (record: Record<string, unknown>) => PeriodPartition | undefined>>;
29
+ }
6
30
  /**
7
31
  * Build the default periods strategy. Pass into
8
32
  * `createNoydb({ periodsStrategy: withPeriods() })` to enable
9
33
  * `vault.closePeriod()` / `vault.openPeriod()` / write-guards.
34
+ *
35
+ * Pass `subjects` to run more than one close calendar in a single vault — see
36
+ * {@link WithPeriodsOptions.subjects}.
10
37
  */
11
- export declare function withPeriods(): PeriodsStrategy;
38
+ export declare function withPeriods(options?: WithPeriodsOptions): PeriodsStrategy;
@@ -13,8 +13,10 @@
13
13
  * Vault can call them without TypeScript barrel gymnastics.
14
14
  */
15
15
  export { withPeriods } from './active.js';
16
+ export type { WithPeriodsOptions } from './active.js';
16
17
  export type { PeriodsStrategy } from './strategy.js';
17
- export { PERIODS_COLLECTION, PERIOD_FREEZES_COLLECTION, PERIOD_ARCHIVES_COLLECTION, PERIOD_TARGET_PURGES_COLLECTION, periodExclusiveUpperBound, loadPeriods, chainAnchor, assertTsWritable, validatePeriodName, appendPeriodLedgerEntry, purgeMarkersOn, } from './periods.js';
18
- export type { PeriodRecord, PeriodFreezeRecord, PeriodArchiveRecord, PeriodTargetPurgeRecord, TargetPurgeCount, ClosePeriodOptions, OpenPeriodOptions, CarryForwardContext, ReadOnlyCollection, } from './periods.js';
18
+ export type { PeriodScope } from './vault-facade.js';
19
+ export { PERIODS_COLLECTION, PERIOD_FREEZES_COLLECTION, PERIOD_ARCHIVES_COLLECTION, PERIOD_TARGET_PURGES_COLLECTION, periodExclusiveUpperBound, resolvePeriodKey, samePartition, loadPeriods, chainAnchor, assertTsWritable, validatePeriodName, appendPeriodLedgerEntry, purgeMarkersOn, } from './periods.js';
20
+ export type { PeriodPartition, PartitionResolver, PeriodRecord, PeriodFreezeRecord, PeriodArchiveRecord, PeriodTargetPurgeRecord, TargetPurgeCount, ClosePeriodOptions, OpenPeriodOptions, CarryForwardContext, ReadOnlyCollection, } from './periods.js';
19
21
  /** The un-opted-in stub for this service — exported so callers can compare against it (#844). */
20
22
  export { NO_PERIODS } from './strategy.js';
@@ -189,6 +189,46 @@ export interface PeriodTargetPurgeRecord {
189
189
  readonly purgedBy: string;
190
190
  readonly targets: readonly TargetPurgeCount[];
191
191
  }
192
+ /**
193
+ * Scope tuple for a period timeline (#1005).
194
+ *
195
+ * Identical in shape and semantics to `SequenceOptions.partition`: a
196
+ * partitioned timeline is always disjoint from any unpartitioned one, and from
197
+ * every other tuple. `['acme', 'vat']` and `['acme', 'wht']` are two
198
+ * independent close calendars for the same subject — which is the whole point,
199
+ * since sub-ledgers for one legal entity and one month routinely close on
200
+ * different statutory deadlines.
201
+ */
202
+ export type PeriodPartition = readonly (string | number)[];
203
+ /**
204
+ * Resolve the `_periods` storage key for a (name, partition) pair.
205
+ *
206
+ * Deliberately the same encoding as `resolveSequenceKey`: `name` verbatim when
207
+ * unpartitioned, else `${name}\x00${parts}` with each component
208
+ * `encodeURIComponent`d and `'/'`-joined. The null-byte separator cannot occur
209
+ * in a period name, so a partitioned key never collides with an unpartitioned
210
+ * one; URI-encoding keeps `['a/b']` distinct from `['a','b']`.
211
+ *
212
+ * @throws {ValidationError} on an empty component or a non-finite number.
213
+ * @internal
214
+ */
215
+ export declare function resolvePeriodKey(name: string, partition?: PeriodPartition): string;
216
+ /**
217
+ * Do two partitions denote the same timeline? Absent and empty both mean "the
218
+ * unpartitioned timeline", so they compare equal.
219
+ *
220
+ * @internal
221
+ */
222
+ export declare function samePartition(a?: PeriodPartition, b?: PeriodPartition): boolean;
223
+ /**
224
+ * Resolves a record to the timeline that governs it. Supplied by
225
+ * `withPeriods({ subjects })`; returns `undefined` for any collection with no
226
+ * mapping, which is what keeps an unconfigured vault on the single vault-wide
227
+ * timeline it has always had.
228
+ *
229
+ * @internal
230
+ */
231
+ export type PartitionResolver = (collection: string, record: Record<string, unknown>) => PeriodPartition | undefined;
192
232
  /**
193
233
  * Stored record for one closed or opened accounting period. One entry
194
234
  * per period, keyed by `name` in the reserved `_periods` collection.
@@ -199,8 +239,19 @@ export interface PeriodTargetPurgeRecord {
199
239
  * into the next one, the same way the ledger's `prevHash` works.
200
240
  */
201
241
  export interface PeriodRecord {
202
- /** Human-readable name (e.g., `'FY2026-Q1'`). Unique per vault. */
242
+ /**
243
+ * Human-readable name (e.g., `'FY2026-Q1'`). Unique per PARTITION — two
244
+ * timelines may each carry a `'2026-06'`, which is the normal case when one
245
+ * vault serves several subjects (#1005). Unique per vault when unpartitioned.
246
+ */
203
247
  readonly name: string;
248
+ /**
249
+ * The timeline this period belongs to. Absent = the vault-wide timeline.
250
+ * Two periods with the same `name` and different `partition` are unrelated:
251
+ * separate hash chains, separate close state, and the write guard applies
252
+ * each only to records that resolve to its own tuple.
253
+ */
254
+ readonly partition?: PeriodPartition;
204
255
  /**
205
256
  * Role discriminator. A period is `'closed'` from the moment its
206
257
  * `closedAt` is recorded; `'opened'` marks a period whose opening
@@ -283,11 +334,32 @@ export interface ClosePeriodOptions {
283
334
  * an explicit `dateField`.
284
335
  */
285
336
  readonly dateField?: string;
337
+ /**
338
+ * Close only this timeline (#1005). Omit for the vault-wide timeline.
339
+ *
340
+ * ```ts
341
+ * vault.closePeriod({
342
+ * name: '2026-06', endDate: '2026-06-30', dateField: 'issuedAt',
343
+ * partition: [clientId, 'vat'],
344
+ * })
345
+ * ```
346
+ *
347
+ * Which records the resulting seal applies to is decided by the
348
+ * `subjects` map passed to `withPeriods()` — without one, no record ever
349
+ * resolves to a partition and a partitioned close seals nothing.
350
+ */
351
+ readonly partition?: PeriodPartition;
286
352
  }
287
353
  /** Options for `vault.openPeriod()`. */
288
354
  export interface OpenPeriodOptions<TCollections = Record<string, Record<string, unknown>>> {
289
355
  /** Human-readable name for the new period. Must be unique. */
290
356
  readonly name: string;
357
+ /**
358
+ * The timeline to open in. Must match the partition of `fromPeriod` — a
359
+ * period cannot chain across timelines, since each carries its own hash
360
+ * chain (#1005).
361
+ */
362
+ readonly partition?: PeriodPartition;
291
363
  /** ISO lower bound of the new period (usually prior `endDate + 1 day`). */
292
364
  readonly startDate: string;
293
365
  /**
@@ -357,7 +429,7 @@ export declare function loadPeriods(adapter: NoydbStore, vault: string, decrypt:
357
429
  *
358
430
  * @internal
359
431
  */
360
- export declare function chainAnchor(records: readonly PeriodRecord[]): Promise<{
432
+ export declare function chainAnchor(records: readonly PeriodRecord[], partition?: PeriodPartition): Promise<{
361
433
  priorPeriodName?: string;
362
434
  priorPeriodHash: string;
363
435
  }>;
@@ -382,7 +454,10 @@ export declare function chainAnchor(records: readonly PeriodRecord[]): Promise<{
382
454
  export declare function assertTsWritable(existing: {
383
455
  ts: string | null;
384
456
  record: Record<string, unknown> | null;
385
- } | null, incomingRecord: Record<string, unknown> | null, closedPeriods: readonly PeriodRecord[]): void;
457
+ } | null, incomingRecord: Record<string, unknown> | null, closedPeriods: readonly PeriodRecord[], scope?: {
458
+ collection: string;
459
+ resolve?: PartitionResolver;
460
+ }): void;
386
461
  /**
387
462
  * Sanity-check a proposed period name + endDate against existing
388
463
  * records. Shared by closePeriod / openPeriod so the two pathways
@@ -390,7 +465,7 @@ export declare function assertTsWritable(existing: {
390
465
  *
391
466
  * @internal
392
467
  */
393
- export declare function validatePeriodName(name: string, existing: readonly PeriodRecord[]): void;
468
+ export declare function validatePeriodName(name: string, existing: readonly PeriodRecord[], partition?: PeriodPartition): void;
394
469
  /**
395
470
  * Wire a reserved-collection ledger append for a period record. The
396
471
  * period itself is stored via the adapter as an encrypted envelope;
@@ -12,21 +12,30 @@
12
12
  */
13
13
  import type { EncryptedEnvelope, NoydbStore } from '../../kernel/types.js';
14
14
  import type { LedgerStore } from '../../with-commit/history/ledger/store.js';
15
- import type { PeriodRecord } from './periods.js';
15
+ import type { PeriodRecord, PeriodPartition, PartitionResolver } from './periods.js';
16
16
  /**
17
17
  * @internal
18
18
  */
19
19
  export interface PeriodsStrategy {
20
20
  loadPeriods(adapter: NoydbStore, vault: string, decrypt: (envelope: EncryptedEnvelope) => Promise<PeriodRecord>): Promise<PeriodRecord[]>;
21
- chainAnchor(records: readonly PeriodRecord[]): Promise<{
21
+ chainAnchor(records: readonly PeriodRecord[], partition?: PeriodPartition): Promise<{
22
22
  priorPeriodName?: string;
23
23
  priorPeriodHash: string;
24
24
  }>;
25
25
  assertTsWritable(existing: {
26
26
  ts: string | null;
27
27
  record: Record<string, unknown> | null;
28
- } | null, incoming: Record<string, unknown> | null, periods: readonly PeriodRecord[]): void;
29
- validatePeriodName(name: string, existing: readonly PeriodRecord[]): void;
28
+ } | null, incoming: Record<string, unknown> | null, periods: readonly PeriodRecord[], scope?: {
29
+ collection: string;
30
+ resolve?: PartitionResolver;
31
+ }): void;
32
+ validatePeriodName(name: string, existing: readonly PeriodRecord[], partition?: PeriodPartition): void;
33
+ /**
34
+ * Record → timeline resolver built from `withPeriods({ subjects })` (#1005).
35
+ * `undefined` when the caller configured no subjects, which keeps every
36
+ * record on the vault-wide timeline.
37
+ */
38
+ readonly partitionOf?: PartitionResolver;
30
39
  appendPeriodLedgerEntry(ledger: LedgerStore | null, actor: string, envelope: EncryptedEnvelope, periodName: string, collection?: string): Promise<void>;
31
40
  }
32
41
  export declare const NO_PERIODS: PeriodsStrategy;
@@ -3,7 +3,16 @@ import type { NoydbStore } from '../../kernel/types.js';
3
3
  import type { LedgerStore } from '../../with-commit/history/ledger/store.js';
4
4
  import type { Collection } from '../../kernel/collection.js';
5
5
  import type { PeriodsStrategy } from './strategy.js';
6
- import { type PeriodRecord, type TargetPurgeCount, type ClosePeriodOptions, type OpenPeriodOptions } from './periods.js';
6
+ import { type PeriodRecord, type TargetPurgeCount, type ClosePeriodOptions, type OpenPeriodOptions, type PeriodPartition } from './periods.js';
7
+ /** Selects one period timeline. Omit — or pass an empty tuple — for the vault-wide one (#1005). */
8
+ export interface PeriodScope {
9
+ readonly partition?: PeriodPartition;
10
+ }
11
+ /** The persisted side of a write, as the period write-guard sees it. */
12
+ export interface PeriodGuardPrior {
13
+ readonly ts: string | null;
14
+ readonly record: Record<string, unknown> | null;
15
+ }
7
16
  /** Everything the moving period methods touched on the vault's `this.*`. */
8
17
  export interface VaultPeriodsDeps {
9
18
  /** Resolved periods strategy (NO_PERIODS when not configured). */
@@ -56,7 +65,7 @@ export declare class VaultPeriods {
56
65
  * read only. Idempotent: a second call is a no-op that returns the
57
66
  * same merged record without re-purging or re-appending a ledger entry.
58
67
  */
59
- freezePeriod(name: string): Promise<PeriodRecord>;
68
+ freezePeriod(name: string, options?: PeriodScope): Promise<PeriodRecord>;
60
69
  /**
61
70
  * Archive a closed period (#613): physically relocates its in-window
62
71
  * records (those with `_ts < periodExclusiveUpperBound(endDate)`) from the
@@ -66,7 +75,7 @@ export declare class VaultPeriods {
66
75
  * through to cold) and idempotent: a second call is a no-op returning the
67
76
  * same merged record.
68
77
  */
69
- archivePeriod(name: string): Promise<PeriodRecord>;
78
+ archivePeriod(name: string, options?: PeriodScope): Promise<PeriodRecord>;
70
79
  /**
71
80
  * Target-purge a closed period (#615): sweeps delete markers off the vault's
72
81
  * PUSH-ONLY sync targets (`backup`/`archive`) via `purgeTargets`, recording a
@@ -76,22 +85,39 @@ export declare class VaultPeriods {
76
85
  * Idempotent once run; with no push-only targets it writes no companion and
77
86
  * is re-runnable.
78
87
  */
79
- purgePeriodTargets(name: string): Promise<PeriodRecord>;
88
+ purgePeriodTargets(name: string, options?: PeriodScope): Promise<PeriodRecord>;
80
89
  /** Merge target-purge companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
81
90
  private mergeTargetPurge;
82
91
  /** Merge archive companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
83
92
  private mergeArchive;
84
93
  /** Merge freeze companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
85
94
  private mergeFreeze;
86
- /** Return every closed / opened period in `closedAt` order, merged with any freeze + archive companions. */
87
- listPeriods(): Promise<readonly PeriodRecord[]>;
88
- /** Look up a single period by name, merged with its freeze + archive + target-purge companions if any. Returns `null` if not found. */
89
- getPeriod(name: string): Promise<PeriodRecord | null>;
90
- /** Called by the gate bus before put/delete. */
95
+ /**
96
+ * Return every closed / opened period in `closedAt` order, merged with any
97
+ * freeze + archive companions.
98
+ *
99
+ * With no argument this spans EVERY timeline — the pre-#1005 behaviour, and
100
+ * the right default for an audit sweep. Pass `{ partition }` to scope to one
101
+ * timeline; `{ partition: [] }` (or omitting it) means the vault-wide one.
102
+ */
103
+ listPeriods(options?: PeriodScope): Promise<readonly PeriodRecord[]>;
104
+ /**
105
+ * Look up a single period by name within one timeline, merged with its
106
+ * freeze + archive + target-purge companions if any. Returns `null` if not
107
+ * found.
108
+ *
109
+ * Names are only unique per partition (#1005), so this resolves against the
110
+ * VAULT-WIDE timeline unless `{ partition }` says otherwise — matching how
111
+ * every pre-partition period is stored.
112
+ */
113
+ getPeriod(name: string, options?: PeriodScope): Promise<PeriodRecord | null>;
114
+ /** Resolve one period within the timeline named by `options` (vault-wide when absent). */
115
+ private findPeriod;
116
+ /** Called by the gate bus before put/delete. `collection` selects the subject mapping (#1005). */
91
117
  assertTsWritable(existing: {
92
118
  ts: string | null;
93
119
  record: Record<string, unknown> | null;
94
- } | null, incoming: Record<string, unknown> | null): Promise<void>;
120
+ } | null, incoming: Record<string, unknown> | null, collection?: string): Promise<void>;
95
121
  private loadPeriodsCache;
96
122
  /** Generic reserved-collection writer — serves `_periods` and `_period_freezes` alike. */
97
123
  private writeReserved;
@@ -4,6 +4,7 @@ import type { ReduceSpec, Reduction } from '../../with-lookup/reduce/reduction.j
4
4
  import type { GroupedReduction } from '../../with-lookup/reduce/groupby.js';
5
5
  import type { JoinStrategy } from '../../kernel/query/join.js';
6
6
  import type { MoneyDescriptor } from '../../via/money/descriptor.js';
7
+ import type { ExactMath } from '../../via/money/exact.js';
7
8
  import type { I18nTextDescriptor } from '../../via/i18n/core.js';
8
9
  /**
9
10
  * Minimal vault-shaped accessor passed to the MV `query()` callback.
@@ -269,6 +270,57 @@ export interface MaterializedViewSpec<TRow extends Record<string, unknown>> {
269
270
  * UNION-mode only. Ignored if {@link query} is set.
270
271
  */
271
272
  aggregate?: ReduceSpec;
273
+ /**
274
+ * Post-aggregate projection over ONE finished row (#1007).
275
+ *
276
+ * `aggregate` accepts reducers only, so a row can carry every input a
277
+ * derived value needs and still not express it — `max(0, netTotal - paid)`
278
+ * is not a reduction. `derive` closes that gap: it receives the row the
279
+ * grouping/aggregation pipeline just produced and returns a patch merged
280
+ * onto it, immediately before materialisation.
281
+ *
282
+ * ```ts
283
+ * aggregate: { paid: sum('paid'), netTotal: sum('netTotal') },
284
+ * derive: (row) => ({ toPay: Math.max(0, row.netTotal - row.paid) }),
285
+ * ```
286
+ *
287
+ * Deliberately narrow, and the narrowness is what keeps it safe under
288
+ * incremental recompute: **pure, single-row, no cross-row access, no second
289
+ * aggregation pass.** It only ever sees the row the reducer just produced,
290
+ * so a refresh triggered by one source write recomputes it correctly without
291
+ * the engine needing to know anything about the function.
292
+ *
293
+ * Returning `null` / `undefined` leaves the row unchanged. Returned keys
294
+ * that collide with a {@link groupBy} field throw
295
+ * `MaterializedViewConfigError` — a group key is the row's identity and
296
+ * feeds {@link rowKey}, so rewriting it would silently re-home the row.
297
+ *
298
+ * **Money:** money leaves a reducer as an exact decimal string, and
299
+ * `Number(a) - Number(b)` would put it straight back through binary floating
300
+ * point — `10.05 - 0.10` becomes `9.950000000000001`, which the money
301
+ * quantiser then refuses rather than storing drift. So `derive` receives
302
+ * {@link ExactMath} as its second argument; it works in scaled BigInt and
303
+ * cannot introduce a representation error:
304
+ *
305
+ * ```ts
306
+ * moneyFields: { netTotal: THB, paid: THB, toPay: THB },
307
+ * derive: (row, exact) => ({ toPay: exact.max(0, exact.sub(row.netTotal, row.paid)) }),
308
+ * ```
309
+ *
310
+ * Declare the derived field in {@link moneyFields} and the result is
311
+ * quantised through that descriptor before storage, exact at its scale.
312
+ *
313
+ * Note the one place `TRow` under-describes what arrives: a field declared
314
+ * in {@link moneyFields} reaches `derive` DECODED — the decimal string a
315
+ * reader would see, not the scaled integer the reducer left behind — even
316
+ * where `TRow` types it as `number` (which is what the arms' `map` emits).
317
+ * `ExactMath` accepts both, which is why its operations are the right tool
318
+ * here and why no cast is needed.
319
+ *
320
+ * Applies to every MV form (union, projection, and query), always as the
321
+ * last step before rows are materialised.
322
+ */
323
+ derive?: (row: TRow, exact: ExactMath) => Record<string, unknown> | null | undefined;
272
324
  /**
273
325
  * Money descriptors for the UNION-mode aggregate, keyed by the
274
326
  * OUTPUT/intermediate field name as it appears in the mapped row and
@@ -368,8 +368,17 @@ export declare function listUsersWithEnvelopes<T = unknown>(store: NoydbStore, v
368
368
  user: UserInfo;
369
369
  envelope: UserEnvelope<T> | null;
370
370
  }>>;
371
- /** Ensure a DEK exists for a collection. Generates one if new. */
372
371
  export declare function ensureCollectionDEK(store: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<(collectionName: string) => Promise<EnclaveKey>>;
372
+ /**
373
+ * The `Permissions` catch-all key (#1010). `Permissions` has always documented
374
+ * `'*'` as "the wildcard collection matching all collections in the vault", but
375
+ * nothing expanded it — a grantee handed the documented catch-all got no keys
376
+ * at all. It is honoured in three places, and they must agree: the DEK wrapping
377
+ * in `grant()`, and both permission checks below.
378
+ */
379
+ export declare const PERMISSION_WILDCARD = "*";
380
+ /** Does this permission map hand over the whole vault? */
381
+ export declare function permissionsAreWildcard(permissions: Permissions | undefined): boolean;
373
382
  /** Check if a user has write permission for a collection. */
374
383
  export declare function hasWritePermission(keyring: UnlockedKeyring, collectionName: string): boolean;
375
384
  /** Check if a user has any access to a collection. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/hub",
3
- "version": "0.6.0-pre.4",
3
+ "version": "0.6.0-pre.7",
4
4
  "description": "Zero-knowledge, offline-first, encrypted document store — core library with AES-256-GCM, PBKDF2, multi-user keyring, and sync engine",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -210,14 +210,14 @@
210
210
  "node": ">=22.0.0"
211
211
  },
212
212
  "dependencies": {
213
- "@noy-db/attestation": "0.6.0-pre.4"
213
+ "@noy-db/attestation": "0.6.0-pre.7"
214
214
  },
215
215
  "devDependencies": {
216
216
  "@types/node": "^22.0.0",
217
217
  "esbuild": "^0.25.0",
218
218
  "zod": "^4.0.0",
219
219
  "zod-to-json-schema": "^3.25.2",
220
- "@noy-db/on-shamir": "0.6.0-pre.4"
220
+ "@noy-db/on-shamir": "0.6.0-pre.7"
221
221
  },
222
222
  "peerDependencies": {
223
223
  "zod-to-json-schema": "^3.25.0"