@noy-db/hub 0.7.0-pre.11 → 0.7.0-pre.13

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 (98) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/as/index.js +3 -2
  3. package/dist/as/index.js.map +1 -1
  4. package/dist/{backup-VASRVLDZ.js → backup-R6A4VZRX.js} +4 -4
  5. package/dist/cargo/index.js +2 -2
  6. package/dist/chunk-66TQOPFM.js +37 -0
  7. package/dist/chunk-66TQOPFM.js.map +1 -0
  8. package/dist/{chunk-OEA5NVSU.js → chunk-AJQY44CE.js} +86 -6
  9. package/dist/chunk-AJQY44CE.js.map +1 -0
  10. package/dist/{chunk-S5AMPTNE.js → chunk-BMELYFHU.js} +2 -2
  11. package/dist/{chunk-T6PSWLAR.js → chunk-BUE2D7PX.js} +25 -2
  12. package/dist/chunk-BUE2D7PX.js.map +1 -0
  13. package/dist/chunk-BWXGSSFX.js +1589 -0
  14. package/dist/chunk-BWXGSSFX.js.map +1 -0
  15. package/dist/{chunk-HN4FNA4X.js → chunk-COTPDJR5.js} +8 -8
  16. package/dist/{chunk-E3EBMC4G.js → chunk-E2MYJGSG.js} +2 -2
  17. package/dist/{chunk-JJR2JIN4.js → chunk-FFBYRWDJ.js} +2 -2
  18. package/dist/{chunk-WQF3QABE.js → chunk-IHFXR2VQ.js} +9 -79
  19. package/dist/chunk-IHFXR2VQ.js.map +1 -0
  20. package/dist/chunk-MGOXJIBV.js +176 -0
  21. package/dist/chunk-MGOXJIBV.js.map +1 -0
  22. package/dist/chunk-PDD75NUZ.js +87 -0
  23. package/dist/chunk-PDD75NUZ.js.map +1 -0
  24. package/dist/chunk-RTBJ5IOO.js +87 -0
  25. package/dist/chunk-RTBJ5IOO.js.map +1 -0
  26. package/dist/chunk-RXLYKVTF.js +17 -0
  27. package/dist/chunk-RXLYKVTF.js.map +1 -0
  28. package/dist/{chunk-ESVREVWN.js → chunk-SXCYYMVS.js} +2 -2
  29. package/dist/chunk-TFMM5DBB.js +20 -0
  30. package/dist/chunk-TFMM5DBB.js.map +1 -0
  31. package/dist/chunk-TPHAGLUH.js +30 -0
  32. package/dist/chunk-TPHAGLUH.js.map +1 -0
  33. package/dist/{chunk-THNJ3IKU.js → chunk-ULUMAZ5S.js} +2 -2
  34. package/dist/{chunk-NO272T7Q.js → chunk-V4PXLYWM.js} +2 -31
  35. package/dist/chunk-V4PXLYWM.js.map +1 -0
  36. package/dist/{chunk-A5WZEPXJ.js → chunk-VGICZPVE.js} +5 -3
  37. package/dist/{chunk-A5WZEPXJ.js.map → chunk-VGICZPVE.js.map} +1 -1
  38. package/dist/{chunk-KHKKD62N.js → chunk-VYYM3MEN.js} +2 -16
  39. package/dist/chunk-VYYM3MEN.js.map +1 -0
  40. package/dist/{chunk-SWVNZQVR.js → chunk-YPXNAP72.js} +2 -169
  41. package/dist/chunk-YPXNAP72.js.map +1 -0
  42. package/dist/{chunk-I6DGKPTY.js → chunk-ZVCC36XQ.js} +101 -1273
  43. package/dist/chunk-ZVCC36XQ.js.map +1 -0
  44. package/dist/{collection-facade-MGGHQ3HU.js → collection-facade-4ACKIDDY.js} +4 -3
  45. package/dist/derivations/index.js +21 -6
  46. package/dist/dispatch-MSTR5VBI.js +255 -0
  47. package/dist/dispatch-MSTR5VBI.js.map +1 -0
  48. package/dist/{dispatch-44J4TL72.js → dispatch-PDP5TA73.js} +5 -5
  49. package/dist/{executor-KSPGNVDO.js → executor-YDDC5XGY.js} +4 -3
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +39 -35
  52. package/dist/index.js.map +1 -1
  53. package/dist/introspection/index.js +3 -2
  54. package/dist/kernel/collection.d.ts +11 -6
  55. package/dist/kernel/match-pairs.d.ts +30 -0
  56. package/dist/lazy/index.js +3 -2
  57. package/dist/materialized-views/index.js +4 -3
  58. package/dist/{noydb-QJFZAH34.js → noydb-2C6QQJ5S.js} +17 -12
  59. package/dist/pod/index.js +17 -12
  60. package/dist/registry-D2YXKRYC.js +33 -0
  61. package/dist/search/index.js +1 -1
  62. package/dist/{stale-KSPG6LFX.js → stale-2CHKZTMK.js} +2 -2
  63. package/dist/{walk-G3CZIL4L.js → walk-P3E6QSNC.js} +4 -3
  64. package/dist/with-formula/derivations/dispatch.d.ts +16 -1
  65. package/dist/with-formula/derivations/registry.d.ts +20 -6
  66. package/dist/with-formula/derivations/trigger-match.d.ts +52 -0
  67. package/dist/with-formula/derivations/types.d.ts +23 -0
  68. package/dist/with-shape/introspection/behaviors.d.ts +7 -0
  69. package/dist/with-shape/introspection/describe.d.ts +30 -0
  70. package/package.json +3 -3
  71. package/dist/chunk-I6DGKPTY.js.map +0 -1
  72. package/dist/chunk-KHKKD62N.js.map +0 -1
  73. package/dist/chunk-NO272T7Q.js.map +0 -1
  74. package/dist/chunk-OEA5NVSU.js.map +0 -1
  75. package/dist/chunk-SWVNZQVR.js.map +0 -1
  76. package/dist/chunk-T6PSWLAR.js.map +0 -1
  77. package/dist/chunk-UB5CRTFF.js +0 -344
  78. package/dist/chunk-UB5CRTFF.js.map +0 -1
  79. package/dist/chunk-WQF3QABE.js.map +0 -1
  80. package/dist/chunk-YPMW26SL.js +0 -81
  81. package/dist/chunk-YPMW26SL.js.map +0 -1
  82. package/dist/dispatch-IM52HDXT.js +0 -194
  83. package/dist/dispatch-IM52HDXT.js.map +0 -1
  84. package/dist/registry-FZXPQRAB.js +0 -18
  85. /package/dist/{backup-VASRVLDZ.js.map → backup-R6A4VZRX.js.map} +0 -0
  86. /package/dist/{chunk-S5AMPTNE.js.map → chunk-BMELYFHU.js.map} +0 -0
  87. /package/dist/{chunk-HN4FNA4X.js.map → chunk-COTPDJR5.js.map} +0 -0
  88. /package/dist/{chunk-E3EBMC4G.js.map → chunk-E2MYJGSG.js.map} +0 -0
  89. /package/dist/{chunk-JJR2JIN4.js.map → chunk-FFBYRWDJ.js.map} +0 -0
  90. /package/dist/{chunk-ESVREVWN.js.map → chunk-SXCYYMVS.js.map} +0 -0
  91. /package/dist/{chunk-THNJ3IKU.js.map → chunk-ULUMAZ5S.js.map} +0 -0
  92. /package/dist/{collection-facade-MGGHQ3HU.js.map → collection-facade-4ACKIDDY.js.map} +0 -0
  93. /package/dist/{dispatch-44J4TL72.js.map → dispatch-PDP5TA73.js.map} +0 -0
  94. /package/dist/{executor-KSPGNVDO.js.map → executor-YDDC5XGY.js.map} +0 -0
  95. /package/dist/{noydb-QJFZAH34.js.map → noydb-2C6QQJ5S.js.map} +0 -0
  96. /package/dist/{registry-FZXPQRAB.js.map → registry-D2YXKRYC.js.map} +0 -0
  97. /package/dist/{stale-KSPG6LFX.js.map → stale-2CHKZTMK.js.map} +0 -0
  98. /package/dist/{walk-G3CZIL4L.js.map → walk-P3E6QSNC.js.map} +0 -0
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  dumpVaultSchema
3
- } from "./chunk-A5WZEPXJ.js";
4
- import "./chunk-KHKKD62N.js";
3
+ } from "./chunk-VGICZPVE.js";
4
+ import "./chunk-RXLYKVTF.js";
5
+ import "./chunk-VYYM3MEN.js";
5
6
  import "./chunk-URHRIAV6.js";
6
7
  import "./chunk-Y2NBBT6K.js";
7
8
  import "./chunk-WWNYSV7S.js";
@@ -18,4 +19,4 @@ import "./chunk-PZ5AY32C.js";
18
19
  export {
19
20
  dumpVaultSchema
20
21
  };
21
- //# sourceMappingURL=walk-G3CZIL4L.js.map
22
+ //# sourceMappingURL=walk-P3E6QSNC.js.map
@@ -94,5 +94,20 @@ export interface DerivationDispatchCtx extends DerivationDeleteCtx {
94
94
  * by the executor — a write to the child recomputes the parent aggregate, and
95
95
  * a write to the parent recomputes its own.
96
96
  */
97
- export declare function dispatchDerivations(ctx: DerivationDispatchCtx, id: string, record: Record<string, unknown>, version: number, wave?: WaveContext): Promise<void>;
97
+ export declare function dispatchDerivations(ctx: DerivationDispatchCtx, id: string, record: Record<string, unknown>, version: number, wave?: WaveContext,
98
+ /** The pre-write record at `id` in THIS collection (undefined = not
99
+ * captured — no field-match trigger, or a sync-applied wave; null =
100
+ * captured and absent, i.e. a create). Only a `Record` re-fires the OLD
101
+ * matched set alongside the new one (spec §7). */
102
+ prior?: Record<string, unknown> | null): Promise<void>;
103
+ /**
104
+ * Trigger fan-out for a DELETED parent record (#1249, spec §8). Distinct from
105
+ * the "record-shape derivations not dispatched on delete" rule — that is
106
+ * about deleting a SOURCE record; this fires when a TRIGGER collection's
107
+ * record is deleted, re-deriving source records that still exist. Pairs
108
+ * evaluate against the tombstoned record's values; matched sources re-derive
109
+ * through the normal executor (their derive() reads the now-absent parent
110
+ * and decides what that means — the engine never cascades deletes).
111
+ */
112
+ export declare function dispatchTriggerDerivationsOnDelete(ctx: DerivationDispatchCtx, id: string, deleted: Record<string, unknown>): Promise<void>;
98
113
  export {};
@@ -1,15 +1,11 @@
1
1
  import { ViaGraph, type FieldRef, type EdgeKind, type Grain } from '../../kernel/via/graph.js';
2
+ import { type NormalizedTrigger } from './trigger-match.js';
2
3
  import type { DerivationSpec } from './types.js';
3
4
  interface RegisteredStrategy {
4
5
  spec: DerivationSpec<any, any>;
5
6
  strategyHash: string;
7
+ readonly triggers: ReadonlyArray<NormalizedTrigger>;
6
8
  }
7
- /**
8
- * Vault-internal registry of derivation strategies. Owned by `Vault`;
9
- * not exported.
10
- *
11
- * @internal
12
- */
13
9
  export declare class DerivationRegistry {
14
10
  private readonly _bySource;
15
11
  private readonly _byOutput;
@@ -22,6 +18,24 @@ export declare class DerivationRegistry {
22
18
  register(spec: DerivationSpec<any, any>): Promise<void>;
23
19
  strategiesForSource(source: string): ReadonlyArray<RegisteredStrategy>;
24
20
  strategiesProducingOutput(collection: string): ReadonlyArray<RegisteredStrategy>;
21
+ /**
22
+ * The #1253-pattern typo guard for match fields (#1249): a misspelt
23
+ * `to`/`from` silently matches nothing forever, so validate against the
24
+ * collection's enumerable field set at the earliest point it exists.
25
+ * `schemaFieldKeys(schema) === undefined` (TS-generic collection,
26
+ * unreadable validator) is DELIBERATELY silent — those fields are real
27
+ * and unenumerable. `configKeys` folds in the collection's other
28
+ * non-schema field declarations (fieldMeta/moneyFields/dictKeyFields/
29
+ * refs/computed); `denormExempt` is derived here from this registry's
30
+ * own strategies rather than taken as a parameter — a field this
31
+ * collection's own derivations write via `denorm` is never a typo.
32
+ */
33
+ validateFieldsFor(collectionName: string, schema: unknown, configKeys: ReadonlyArray<string>, viaSources?: {
34
+ readonly computed?: Readonly<Record<string, unknown>> | undefined;
35
+ readonly viaFields?: Readonly<Record<string, unknown>> | undefined;
36
+ } | undefined): void;
37
+ /** #1266 — one message shape for both match sides; see {@link matchTargetFieldNames}. */
38
+ private _virtualMatchMessage;
25
39
  /**
26
40
  * All registered strategies as a flat, deduplicated array.
27
41
  * Each strategy is indexed once per source (not once per output key),
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure helpers behind composite `triggerBy` (#1249).
3
+ *
4
+ * A trigger entry is a CONJUNCTION of equality pairs: a source record
5
+ * matches when EVERY pair satisfies String(source[to]) === String(written[from]).
6
+ * `from: 'id'` reads the written record's id (winning over any stored field
7
+ * named `id`, matching dispatch's `{ ...incoming, id }` convention). The
8
+ * legacy `on` form normalizes to `[{ from: 'id', to: on }]` so everything
9
+ * downstream has ONE shape.
10
+ *
11
+ * Scalar coercion is verbatim from `_findMatchingIds`: both sides must be
12
+ * string | number; anything else fails the pair (never throws).
13
+ * Spec: docs/superpowers/specs/2026-08-29-composite-triggerby-design.md §4-§5.
14
+ * @module
15
+ */
16
+ export interface MatchPair {
17
+ readonly from: string;
18
+ readonly to: string;
19
+ }
20
+ export interface NormalizedTrigger {
21
+ readonly collection: string;
22
+ readonly match: ReadonlyArray<MatchPair>;
23
+ readonly maxFanout?: number;
24
+ }
25
+ interface RawTrigger {
26
+ readonly collection: string;
27
+ readonly on?: string;
28
+ readonly match?: ReadonlyArray<MatchPair>;
29
+ readonly maxFanout?: number;
30
+ }
31
+ export declare function normalizeTriggerBy(triggerBy: ReadonlyArray<RawTrigger> | undefined): NormalizedTrigger[];
32
+ /**
33
+ * The value tuple a written record presents to one trigger entry.
34
+ * `null` means "this record cannot address any source" (a from-field is
35
+ * absent or non-scalar) — a legitimate no-match, not an error.
36
+ */
37
+ export declare function tupleFromWritten(match: ReadonlyArray<MatchPair>, writtenId: string, record: Record<string, unknown> | null): Array<{
38
+ field: string;
39
+ value: string;
40
+ }> | null;
41
+ /** Any single component differing means NOT the same tuple (spec §7). */
42
+ export declare function sameTuple(a: Array<{
43
+ field: string;
44
+ value: string;
45
+ }> | null, b: Array<{
46
+ field: string;
47
+ value: string;
48
+ }> | null): boolean;
49
+ /** Moved to the kernel (#1249) so `collection.ts` can import statically without
50
+ * dragging derivations code into the floor bundle; re-exported here for
51
+ * compatibility with existing consumers of this module. */
52
+ export { recordMatchesPairs, findMatchingIdsByPairs } from '../../kernel/match-pairs.js';
@@ -183,6 +183,29 @@ export interface DerivationSpec<TSource extends Record<string, unknown>, TOutput
183
183
  collection: string;
184
184
  on: string;
185
185
  maxFanout?: number;
186
+ } | {
187
+ /**
188
+ * Multi-field trigger (#1249): a source record matches when EVERY
189
+ * pair satisfies `String(source[to]) === String(written[from])`.
190
+ * `from: 'id'` reads the written record's id; any other `from`
191
+ * reads the written record's field. Exactly one of `on` | `match`
192
+ * per entry. On a LOCAL UPDATE that changes any matched `from`
193
+ * field, fan-out runs on old-match ∪ new-match (any single
194
+ * component changing counts) — the prior record is threaded only
195
+ * from the local write path, so a sync-applied wave write or a
196
+ * tiers restore fans out on the new tuple only. Parent DELETES fan
197
+ * out using the tombstoned record's values — for BOTH forms. A
198
+ * missing/non-scalar `from` value matches nothing. Match fields
199
+ * are validated against the collections' enumerable field sets
200
+ * where possible (silent for TS-generic collections — see the
201
+ * registration guard).
202
+ */
203
+ collection: string;
204
+ match: ReadonlyArray<{
205
+ from: string;
206
+ to: string;
207
+ }>;
208
+ maxFanout?: number;
186
209
  }>;
187
210
  /**
188
211
  * @internal — set by `withRollup()`. Marks this strategy as
@@ -46,6 +46,13 @@ export interface DerivationBehaviorEntry {
46
46
  readonly collection: string;
47
47
  readonly on: string;
48
48
  readonly maxFanout?: number;
49
+ } | {
50
+ readonly collection: string;
51
+ readonly match: ReadonlyArray<{
52
+ readonly from: string;
53
+ readonly to: string;
54
+ }>;
55
+ readonly maxFanout?: number;
49
56
  }>;
50
57
  readonly rollup?: {
51
58
  readonly from: string;
@@ -206,6 +206,36 @@ export interface ZodFieldSlot {
206
206
  * A duck-typed probe, not a Zod dependency: an unrecognised validator returns
207
207
  * `undefined` and the caller stays silent rather than guessing, so a validator
208
208
  * hub cannot read never produces a false "unknown field" error.
209
+ *
210
+ * Wrappers (reported by the pilot on #1249's guard, and it equally affected
211
+ * #1253's): a wrapped object's `.shape` is undefined, so "has a schema" and
212
+ * "hub can enumerate its fields" silently diverged for exactly the schemas
213
+ * most worth guarding — validated ones.
214
+ *
215
+ * ONE RULE UNWRAPS ALL OF THEM: FOLLOW THE OUTPUT SIDE. These keys describe
216
+ * the PARSED record, so the only question a wrapper raises is whether it
217
+ * changes the parsed shape.
218
+ *
219
+ * Zod 3 `ZodEffects` — `.refine()`/`.superRefine()` (`effect.type` is
220
+ * `'refinement'`) and `z.preprocess()` (`'preprocess'`) both parse WITH
221
+ * the inner schema, so the output is the inner object: follow
222
+ * `_def.schema`. `.transform()` REPLACES the output, so the inner keys
223
+ * would be a lie: stay `undefined`, deliberately.
224
+ * Zod 4 `ZodPipe` — `z.preprocess()` is `pipe(transform -> object)` and
225
+ * `.transform()` is `pipe(object -> transform)`. Following `_def.out`
226
+ * resolves both correctly with no effect-kind test: preprocess reaches the
227
+ * object, transform reaches a `ZodTransform` that has no shape and no
228
+ * inner schema, so the loop returns `undefined` on its own.
229
+ *
230
+ * `preprocess` was the pilot's second finding (#1262): the first fix followed
231
+ * refinements only, which left four of their registered collections unguarded
232
+ * — including the one carrying their only `fieldMeta` PII declaration, i.e.
233
+ * precisely the collection #1253 was written for. It is one string away from
234
+ * `transform` and means the opposite thing, so a test pins the two apart.
235
+ *
236
+ * A duck-typed probe throughout: an unrecognised validator returns `undefined`
237
+ * and the caller stays silent rather than guessing. Bounded depth, so a cyclic
238
+ * duck cannot hang us.
209
239
  */
210
240
  export declare function schemaFieldKeys(schema: unknown): readonly string[] | undefined;
211
241
  export declare function deriveZodFields(schema: unknown): Promise<Record<string, ZodFieldSlot>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/hub",
3
- "version": "0.7.0-pre.11",
3
+ "version": "0.7.0-pre.13",
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>",
@@ -233,14 +233,14 @@
233
233
  "node": ">=22.0.0"
234
234
  },
235
235
  "dependencies": {
236
- "@noy-db/attestation": "0.7.0-pre.11"
236
+ "@noy-db/attestation": "0.7.0-pre.13"
237
237
  },
238
238
  "devDependencies": {
239
239
  "@types/node": "^22.0.0",
240
240
  "esbuild": "^0.25.0",
241
241
  "zod": "^4.0.0",
242
242
  "zod-to-json-schema": "^3.25.2",
243
- "@noy-db/on-shamir": "0.7.0-pre.11"
243
+ "@noy-db/on-shamir": "0.7.0-pre.13"
244
244
  },
245
245
  "peerDependencies": {
246
246
  "zod-to-json-schema": "^3.25.0"