@nanobpm/urban 0.81.0 → 0.82.0

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.
@@ -7,7 +7,7 @@ import { runLlmJob } from "./llm.js";
7
7
  import { runInJobContext } from "../execContext.js";
8
8
  import { readLineage } from "../lineage.js";
9
9
  import { LineageStore } from "./lineage-store.js";
10
- import { readModelRegistry } from "../read-model.js";
10
+ import { readModelRegistry, rollupRegistry } from "../read-model.js";
11
11
  import { registerCanonicalProjections } from "./canonical-projections.js";
12
12
  import { InstanceStateStore } from "./instance-state-store.js";
13
13
  import { OpenUserTasksStore } from "./open-user-tasks-store.js";
@@ -99,6 +99,26 @@ function tryLineageStore(app, log) {
99
99
  return undefined;
100
100
  }
101
101
  }
102
+ /**
103
+ * Apply every managed rollup VIEW registered in the process-wide `rollupRegistry` (ADR 0065, #468) to
104
+ * the app's DEFAULT data source, in dependency order (a composed rollup after the rollup it reads).
105
+ * Mirrors {@link tryProvisionReadModelViews}: empty until a sibling task registers a rollup, and
106
+ * absent-/error-safe so a missing datasource or a VIEW-provisioning failure never breaks a worker mount.
107
+ */
108
+ function tryProvisionRollupViews(app, log) {
109
+ if (rollupRegistry.all().length === 0)
110
+ return;
111
+ if (!app.data.hasDefaultSource()) {
112
+ log("debug", "rollups: no default data source; managed VIEW provisioning skipped");
113
+ return;
114
+ }
115
+ try {
116
+ rollupRegistry.ensureViews(app.data.source().db);
117
+ }
118
+ catch (err) {
119
+ log("warn", "rollups: failed to provision managed VIEWs", { error: String(err) });
120
+ }
121
+ }
102
122
  /**
103
123
  * Apply every managed read-model VIEW registered in the process-wide `readModelRegistry` to the app's
104
124
  * DEFAULT data source (ADR 0065, surface #3). A `defineReadModel(...)` declaration compiles to a
@@ -202,6 +222,10 @@ export async function mountWorkers(ctx, app, sched = defaultScheduler()) {
202
222
  // provision the projection tables per source (absent-safe), and finally the read-model VIEWs.
203
223
  registerCanonicalProjections();
204
224
  tryProvisionCanonicalProjections(app, ctx.host.log);
225
+ // Rollup VIEWs (ADR 0065, #468) BEFORE the read-model VIEWs: a read model's key-correlated rollup
226
+ // lookup `LEFT JOIN`s a rollup's `*_counts` VIEW, and a composed rollup reads another rollup's VIEW,
227
+ // so every rollup VIEW (in dependency order) must exist before any dependent VIEW compiles.
228
+ tryProvisionRollupViews(app, ctx.host.log);
205
229
  tryProvisionReadModelViews(app, ctx.host.log);
206
230
  // Cache module loads so a multi-type handler module is imported once.
207
231
  const moduleCache = new Map();
@@ -10,7 +10,7 @@ export type Literal = string | number | boolean | null;
10
10
  * `col` references a column of the BASE row. `pcol` references a column of the CORRELATED projection
11
11
  * row and is only meaningful inside an {@link ExistsExpr} `where` predicate.
12
12
  */
13
- export type Expr = LitExpr | ColExpr | ProjColExpr | CompareExpr | AndExpr | OrExpr | NotExpr | CaseExpr | ExistsExpr;
13
+ export type Expr = LitExpr | ColExpr | ProjColExpr | RollupColExpr | CompareExpr | AndExpr | OrExpr | NotExpr | IsNullExpr | CaseExpr | ExistsExpr;
14
14
  export interface LitExpr {
15
15
  readonly kind: "lit";
16
16
  readonly value: Literal;
@@ -23,6 +23,13 @@ export interface ProjColExpr {
23
23
  readonly kind: "pcol";
24
24
  readonly name: string;
25
25
  }
26
+ export interface RollupColExpr {
27
+ readonly kind: "rcol";
28
+ /** The read-model rollup-lookup alias this column is read from (see {@link RollupLookupDecl}). */
29
+ readonly lookup: string;
30
+ /** The rollup output column referenced (a column of the looked-up `*_counts` rollup row). */
31
+ readonly name: string;
32
+ }
26
33
  export interface CompareExpr {
27
34
  readonly kind: "compare";
28
35
  readonly op: CompareOp;
@@ -41,6 +48,10 @@ export interface NotExpr {
41
48
  readonly kind: "not";
42
49
  readonly expr: Expr;
43
50
  }
51
+ export interface IsNullExpr {
52
+ readonly kind: "isNull";
53
+ readonly expr: Expr;
54
+ }
44
55
  export interface WhenClause {
45
56
  readonly when: Expr;
46
57
  readonly then: Expr;
@@ -63,6 +74,13 @@ export declare const lit: (value: Literal) => LitExpr;
63
74
  export declare const col: (name: string) => ColExpr;
64
75
  /** A reference to a correlated PROJECTION-row column (only valid inside {@link exists}). */
65
76
  export declare const pcol: (name: string) => ProjColExpr;
77
+ /**
78
+ * A reference to a column of a key-correlated ROLLUP LOOKUP row (see {@link ReadModelDecl.lookups}).
79
+ * `rcol("c", "prs_opened")` reads the `prs_opened` column of the rollup joined under alias `"c"` for
80
+ * the base row (compiling to `LEFT JOIN <rollup> ON <key>` on the SQL side / the matching group row on
81
+ * the TS side). Only valid in a {@link defineReadModel} whose `lookups` declares that alias.
82
+ */
83
+ export declare const rcol: (lookup: string, name: string) => RollupColExpr;
66
84
  export declare const eq: (left: Expr, right: Expr) => CompareExpr;
67
85
  export declare const neq: (left: Expr, right: Expr) => CompareExpr;
68
86
  export declare const lt: (left: Expr, right: Expr) => CompareExpr;
@@ -72,6 +90,10 @@ export declare const gte: (left: Expr, right: Expr) => CompareExpr;
72
90
  export declare const and: (...clauses: Expr[]) => AndExpr;
73
91
  export declare const or: (...clauses: Expr[]) => OrExpr;
74
92
  export declare const not: (expr: Expr) => NotExpr;
93
+ /** `<expr> IS NULL` — a null test (1/0 in SQL; `value == null` in TS). `not(isNull(x))` is `IS NOT NULL`. */
94
+ export declare const isNull: (expr: Expr) => IsNullExpr;
95
+ /** Sugar for `not(isNull(expr))` — `<expr> IS NOT NULL`. */
96
+ export declare const isNotNull: (expr: Expr) => NotExpr;
75
97
  /** Build a `CASE WHEN … THEN … [WHEN …] ELSE … END` expression. */
76
98
  export declare const caseWhen: (whens: WhenClause[], otherwise: Expr) => CaseExpr;
77
99
  /** Sugar for a single `{ when, then }` clause used by {@link caseWhen}. */
@@ -127,6 +149,19 @@ export interface SqlCompileOptions {
127
149
  readonly baseAlias?: string;
128
150
  /** Resolve a projection NAME to its physical SQL table/view. Defaults to {@link projectionRegistry}. */
129
151
  readonly resolveProjectionTable?: (name: string) => string;
152
+ /**
153
+ * Resolve a {@link col} reference to its qualified SQL expression, overriding the default
154
+ * `<baseAlias>."name"`. A rollup over a key-JOIN uses this to map a flat output-column name onto the
155
+ * correct joined relation (`col("pr_status")` → `"r"."status"`) so the closed {@link Expr} predicate
156
+ * machinery is reused unchanged across single-relation and join sources.
157
+ */
158
+ readonly resolveColumn?: (name: string) => string;
159
+ /**
160
+ * Resolve an {@link rcol} reference (a key-correlated rollup-lookup column) to its qualified SQL
161
+ * expression. {@link defineReadModel} supplies this from its `lookups` so the lookup's NULL default
162
+ * (a LEFT-JOIN miss) compiles to `COALESCE(<alias>."name", <default>)`.
163
+ */
164
+ readonly resolveRollupColumn?: (lookup: string, name: string) => string;
130
165
  }
131
166
  /** A conservative SQL identifier guard — we interpolate names directly into DDL/SQL. This is the
132
167
  * single source of truth for "is this a legal identifier"; the manifest validator imports it (rather
@@ -155,8 +190,17 @@ export declare function isReservedObjectName(name: string): boolean;
155
190
  * {@link compileToFn} produces cannot diverge.
156
191
  */
157
192
  export declare function compileToSqlSelect(expr: Expr, options?: SqlCompileOptions): string;
193
+ /** The resolved single rollup-lookup rows for one evaluation, keyed by the FOLDED (case-insensitive,
194
+ * SQLite-folded) lookup alias — see {@link rcol}, whose `node.lookup` is folded the same way on read.
195
+ * Every declared lookup alias maps to exactly one resolved row: the matched rollup group with its
196
+ * missing/NULL columns already filled from the lookup's declared defaults (the TS twin of the VIEW's
197
+ * `COALESCE(alias.col, default)`). A LEFT-JOIN miss is therefore represented as a defaults/NULL-filled
198
+ * row — NOT an absent alias — so `rcol` always reads a stable object. Framework resolvers
199
+ * ({@link ReadModel.evaluate}, the parity guard) populate this; a raw `fnFor` caller on a model with
200
+ * lookups must pass the resolved map. */
201
+ export type LookupRows = Record<string, Record<string, unknown>>;
158
202
  /** The compiled runtime derivation: same value as the SQL VIEW, computed in-process. */
159
- export type DerivationFn = (baseRow: BaseRow, projections?: ProjectionRows) => unknown;
203
+ export type DerivationFn = (baseRow: BaseRow, projections?: ProjectionRows, lookups?: LookupRows) => unknown;
160
204
  /**
161
205
  * Compile ONE derivation expression to a TS function computing the SAME value as {@link compileToSqlSelect}.
162
206
  * The function reads the base row for `col`, and — for `exists` — the `projections` argument for the
@@ -187,6 +231,37 @@ export interface ReadModelDecl {
187
231
  * ones. Defaults to true (a read model is usually the base row PLUS its derived columns).
188
232
  */
189
233
  readonly selectBaseColumns?: boolean;
234
+ /**
235
+ * Optional key-correlated ROLLUP LOOKUPS (ADR 0065, #468). Each declares a `LEFT JOIN <rollup> ON
236
+ * <key>` so a derived column can read the rollup's `*_counts` columns for the base row via
237
+ * {@link rcol}. This is the per-row half of the layering: the aggregate is single-sourced in
238
+ * {@link defineRollup}; the read model consumes its columns for a `CASE`-style per-row signal (the
239
+ * `plan_delivery`/`plan_wave_progress` shape). The join `on` must map the rollup's FULL group key so
240
+ * the lookup is single-valued (no fan-out of base rows).
241
+ */
242
+ readonly lookups?: readonly RollupLookupDecl[];
243
+ }
244
+ /** One key-correlated rollup lookup on a {@link defineReadModel} (see {@link ReadModelDecl.lookups}). */
245
+ export interface RollupLookupDecl {
246
+ /** The alias `rcol(alias, …)` reads this lookup's columns under (a SQL identifier). */
247
+ readonly as: string;
248
+ /** The rollup whose `*_counts` VIEW is joined (its `name` is the physical relation, its columns the
249
+ * readable set). Pass the {@link Rollup} object so the read model can validate referenced columns. */
250
+ readonly rollup: Rollup;
251
+ /**
252
+ * The equi-join key: each `{ base, rollup }` pair equates a base column to a rollup group-key column.
253
+ * Must cover the rollup's ENTIRE `groupBy` so the joined row is unique (≤1 match per base row).
254
+ */
255
+ readonly on: ReadonlyArray<{
256
+ readonly base: string;
257
+ readonly rollup: string;
258
+ }>;
259
+ /**
260
+ * Optional per-column defaults substituted when the join misses (no matching rollup group) or the
261
+ * rollup value is NULL — the `COALESCE(<alias>."col", <default>)` in the hand-authored VIEWs. Applied
262
+ * identically in both backends so an epic with no PRs reads `0` rather than NULL.
263
+ */
264
+ readonly defaults?: Record<string, Literal>;
190
265
  }
191
266
  /** A compiled read model: the declaration plus its derived SQL, derived functions, and managed VIEW. */
192
267
  export interface ReadModel {
@@ -197,8 +272,18 @@ export interface ReadModel {
197
272
  sqlSelectFor(column: string, options?: SqlCompileOptions): string;
198
273
  /** The TS derivation function for one derived column. */
199
274
  fnFor(column: string): DerivationFn;
200
- /** Evaluate ALL derived columns for a base row in-process (the TS backend of the whole model). */
201
- evaluate(baseRow: BaseRow, projections?: ProjectionRows): Record<string, unknown>;
275
+ /** Evaluate ALL derived columns for a base row in-process (the TS backend of the whole model). The
276
+ * optional `lookupRows` supplies each rollup-lookup's candidate rows (keyed by lookup alias); the
277
+ * model resolves the single matching group row per lookup (applying declared defaults) before
278
+ * evaluating {@link rcol} references. */
279
+ evaluate(baseRow: BaseRow, projections?: ProjectionRows, lookupRows?: Record<string, ReadonlyArray<Record<string, unknown>>>): Record<string, unknown>;
280
+ /** Resolve each declared rollup lookup's single matching group row for `baseRow` (match on the join
281
+ * keys, fill declared defaults), producing the {@link LookupRows} a {@link DerivationFn} reads via
282
+ * {@link rcol}. The single source of the lookup-resolution rule: {@link evaluate} and the parity guard
283
+ * both call this so they can never drift. Throws if a lookup's candidate set yields MORE than one
284
+ * match for the join key — the lookup is required to be single-valued (its join covers the rollup's
285
+ * full group key), so multiple matches would fan out in SQL while TS silently picked one. */
286
+ resolveLookups(baseRow: BaseRow, lookupRows: Record<string, ReadonlyArray<Record<string, unknown>>>): LookupRows;
202
287
  /** The managed `CREATE VIEW` DDL (seam (a) applies this at boot). Pass `{ temp: true }` to emit a
203
288
  * `CREATE TEMP VIEW` in SQLite's TEMP schema (used by the parity guard so it never touches the
204
289
  * application's real objects); the SELECT body is byte-identical either way. */
@@ -251,10 +336,15 @@ export declare class ReadModelRegistry {
251
336
  * task registers its derived status-edge read model here so the runtime provisions its VIEW at boot.
252
337
  */
253
338
  export declare const readModelRegistry: ReadModelRegistry;
254
- /** A single parity check case: a base row plus the projection rows in scope for it. */
339
+ /** A single parity check case: a base row, the projection rows in scope for it, and (for a read model
340
+ * with rollup lookups) the candidate rollup rows per lookup alias. */
255
341
  export interface ParitySample {
256
342
  readonly baseRow: BaseRow;
257
343
  readonly projections?: ProjectionRows;
344
+ /** Candidate rows for each declared rollup lookup, keyed by lookup alias (see {@link rcol}). The
345
+ * guard inserts them into the lookup rollup's fixture table so the VIEW's `LEFT JOIN` materialises,
346
+ * and hands the same rows to the TS backend to resolve — closing the lookup's parity surface. */
347
+ readonly lookups?: Record<string, ReadonlyArray<Record<string, unknown>>>;
258
348
  }
259
349
  /** The seam a parity guard needs to run the SQL backend: a SQLite handle it can create/query/drop. */
260
350
  export interface ParityDb {
@@ -293,3 +383,176 @@ export interface ParityOptions {
293
383
  * tables/views the caller's DB happens to hold under the same names.
294
384
  */
295
385
  export declare function assertReadModelParity(model: ReadModel, db: ParityDb, samples: ParitySample[], options?: ParityOptions): void;
386
+ /** A closed, flat GROUP-BY aggregate expression (D2). `coalesce`/`add` wrap other aggregates so the
387
+ * exact `COALESCE(MIN(…), MAX(…))` and `MAX(…)+1` shapes the plan VIEWs use are expressible; every
388
+ * variant is handled exhaustively by both backends (SQL + TS reduce). */
389
+ export type AggExpr = CountAgg | CountWhereAgg | MaxAgg | MinWhereAgg | CoalesceAgg | AddAgg;
390
+ export interface CountAgg {
391
+ readonly kind: "count";
392
+ /** When set, `COUNT(col)` (non-NULL rows); omitted → `COUNT(*)` (all group rows). */
393
+ readonly column?: string;
394
+ }
395
+ export interface CountWhereAgg {
396
+ readonly kind: "countWhere";
397
+ /** `SUM(CASE WHEN pred THEN 1 ELSE 0 END)` — count of group rows satisfying the closed predicate. */
398
+ readonly pred: Expr;
399
+ }
400
+ export interface MaxAgg {
401
+ readonly kind: "max";
402
+ readonly column: string;
403
+ }
404
+ export interface MinWhereAgg {
405
+ readonly kind: "minWhere";
406
+ /** `MIN(CASE WHEN pred THEN col END)` — min of `col` over rows satisfying `pred` (NULL if none). */
407
+ readonly column: string;
408
+ readonly pred: Expr;
409
+ }
410
+ export interface CoalesceAgg {
411
+ readonly kind: "coalesce";
412
+ readonly a: AggExpr;
413
+ readonly b: AggExpr;
414
+ }
415
+ export interface AddAgg {
416
+ readonly kind: "add";
417
+ readonly expr: AggExpr;
418
+ /** An INTEGER literal added to `expr` (e.g. `MAX(wave) + 1`). NULL + literal is NULL, both backends. */
419
+ readonly literal: number;
420
+ }
421
+ /** `COUNT(col)` (non-NULL) when `column` is given, else `COUNT(*)`. */
422
+ export declare const count: (column?: string) => CountAgg;
423
+ /** `SUM(CASE WHEN pred THEN 1 ELSE 0 END)` — the count of group rows satisfying `pred`. */
424
+ export declare const countWhere: (pred: Expr) => CountWhereAgg;
425
+ /** `MAX(col)` over the group (ignores NULLs; NULL for an all-NULL/empty group). */
426
+ export declare const max: (column: string) => MaxAgg;
427
+ /** `MIN(CASE WHEN pred THEN col END)` — the min of `col` over group rows satisfying `pred`. */
428
+ export declare const minWhere: (column: string, pred: Expr) => MinWhereAgg;
429
+ /** `COALESCE(a, b)` over two aggregates — `a` unless it is NULL, then `b`. */
430
+ export declare const coalesce: (a: AggExpr, b: AggExpr) => CoalesceAgg;
431
+ /** `expr + <integer literal>` (e.g. `MAX(wave) + 1`). Rejects a non-integer literal at build time. */
432
+ export declare const add: (expr: AggExpr, literal: number) => AddAgg;
433
+ /** The relation a rollup groups over. `table`/`projection`/`rollup` expose their columns by name; a
434
+ * key-`join` exposes a FLAT namespace mapped by {@link RollupJoinSource.columns} (the two-hop reach —
435
+ * D4). All three are single-mechanism composable: a `rollup` source is another {@link Rollup}. */
436
+ export type RollupSource = RollupTableSource | RollupProjectionSource | RollupJoinSource | RollupRollupSource;
437
+ export interface RollupTableSource {
438
+ readonly kind: "table";
439
+ readonly table: string;
440
+ }
441
+ export interface RollupProjectionSource {
442
+ readonly kind: "projection";
443
+ readonly projection: string;
444
+ }
445
+ /** One relation in a {@link RollupJoinSource} — a base table/view/projection under a join alias. */
446
+ export interface RollupRelation {
447
+ readonly relation: string;
448
+ readonly alias: string;
449
+ }
450
+ export interface RollupJoinSource {
451
+ readonly kind: "join";
452
+ /** `"left"` (default) or `"inner"`. LEFT keeps unmatched left rows with NULL right columns. */
453
+ readonly type?: "left" | "inner";
454
+ readonly left: RollupRelation;
455
+ readonly right: RollupRelation;
456
+ /** Equi-join key(s): `left.<left> = right.<right>`. */
457
+ readonly on: ReadonlyArray<{
458
+ readonly left: string;
459
+ readonly right: string;
460
+ }>;
461
+ /**
462
+ * The FLAT output namespace: output column name → `[side, physicalColumn]`. This resolves collisions
463
+ * (both sides may have a `status`) and is what `groupBy`/aggregate `col(...)` references resolve
464
+ * against, so the closed predicate/aggregate machinery is reused unchanged over a join.
465
+ */
466
+ readonly columns: Record<string, readonly [side: "left" | "right", column: string]>;
467
+ }
468
+ export interface RollupRollupSource {
469
+ readonly kind: "rollup";
470
+ readonly rollup: Rollup;
471
+ }
472
+ /** A rollup over a base table. */
473
+ export declare const fromTable: (table: string) => RollupTableSource;
474
+ /** A rollup over a canonical projection (resolved to its physical relation via {@link projectionRegistry}). */
475
+ export declare const fromProjection: (projection: string) => RollupProjectionSource;
476
+ /** A rollup over another rollup's `*_counts` VIEW (composable — e.g. wave's two levels). */
477
+ export declare const fromRollup: (rollup: Rollup) => RollupRollupSource;
478
+ /** A rollup over a declared key-join of two relations (the two-hop reach — D4). */
479
+ export declare const joinSource: (spec: Omit<RollupJoinSource, "kind">) => RollupJoinSource;
480
+ /** The in-memory rows the TS group-reduce sees per LEAF relation (base table/view/projection name). */
481
+ export type RollupInputs = Record<string, ReadonlyArray<Record<string, unknown>>>;
482
+ /** The declarative rollup input passed to {@link defineRollup}. */
483
+ export interface RollupDecl {
484
+ /** The managed VIEW name (also the rollup / canonical-projection identifier). */
485
+ readonly name: string;
486
+ /** The relation to group over (table / projection / key-join / another rollup). */
487
+ readonly source: RollupSource;
488
+ /** One or more group-key columns (referenced by name in the source's output namespace). */
489
+ readonly groupBy: readonly string[];
490
+ /** Optional pre-aggregation row filter (the `WHERE` — e.g. `isNotNull(col("wave"))`). */
491
+ readonly where?: Expr;
492
+ /** The aggregate columns: name → closed {@link AggExpr}. Authored ONCE, compiled to both backends. */
493
+ readonly aggregates: Record<string, AggExpr>;
494
+ }
495
+ /** A compiled rollup: the declaration plus both backends and the managed VIEW DDL. */
496
+ export interface Rollup {
497
+ readonly decl: RollupDecl;
498
+ /** The canonical-projection name (= `decl.name`). */
499
+ readonly projectionName: string;
500
+ /** The group-key columns (order preserved from the declaration). */
501
+ readonly groupBy: readonly string[];
502
+ /** All output columns: the group keys followed by the aggregate names (aggregates sorted by name). */
503
+ readonly outputColumns: readonly string[];
504
+ /** Leaf relation names this rollup reads, transitively (for fixtures / feeding the TS reduce). */
505
+ readonly sourceRelations: readonly string[];
506
+ /** This rollup and every rollup it composes, in dependency order (dependencies first) — the order the
507
+ * managed VIEWs must be created in. */
508
+ readonly viewChain: readonly Rollup[];
509
+ /** The SQLite select-list expression for one aggregate column. */
510
+ sqlAggFor(column: string, options?: SqlCompileOptions): string;
511
+ /** The in-process group-reduce (the TS backend): leaf rows → one output row per group. */
512
+ reduce(inputs: RollupInputs): Record<string, unknown>[];
513
+ /** The managed `CREATE VIEW` DDL. `{ temp: true }` emits a `CREATE TEMP VIEW` (parity-guard use). */
514
+ viewDdl(options?: SqlCompileOptions, viewOptions?: ViewDdlOptions): string;
515
+ /** Fixture column requirements per leaf relation (referenced physical columns), for the parity guard. */
516
+ fixtureColumns(): Map<string, Set<string>>;
517
+ }
518
+ /**
519
+ * Declare a GROUP-BY rollup ONCE (ADR 0065, #468). Returns a {@link Rollup} exposing both backends (the
520
+ * managed `*_counts` VIEW DDL and the TS group-reduce) driven from one closed spec, plus the metadata
521
+ * the parity guard and registry need. Register it in {@link rollupRegistry} to have the runtime apply
522
+ * its VIEW at boot and expose its name as a canonical projection.
523
+ */
524
+ export declare function defineRollup(decl: RollupDecl): Rollup;
525
+ /**
526
+ * The rollup registry (ADR 0065, #468) — the GROUP-BY sibling of {@link ReadModelRegistry}. Registering
527
+ * a {@link Rollup} both (a) queues its managed VIEW for boot provisioning (in dependency order) and (b)
528
+ * registers its name as a canonical projection so read-model rollup lookups resolve. Idempotent: a
529
+ * re-register whose VIEW DDL matches is a no-op; a conflicting redefinition throws.
530
+ */
531
+ export declare class RollupRegistry {
532
+ #private;
533
+ register(rollup: Rollup, projections?: ProjectionRegistry): void;
534
+ all(): Rollup[];
535
+ get(name: string): Rollup | undefined;
536
+ /** Apply every registered rollup's managed VIEW to a database, dependency-ordered (a composed rollup
537
+ * after the rollup it reads) and MANAGED (drop+recreate `main.`-qualified, mirroring
538
+ * {@link ReadModelRegistry.ensureViews}). Safe to call repeatedly. */
539
+ ensureViews(db: {
540
+ exec(sql: string): void;
541
+ }, options?: SqlCompileOptions): void;
542
+ clear(): void;
543
+ }
544
+ /** Process-wide rollup registry — the concrete registration point apps/sidecars import and call. */
545
+ export declare const rollupRegistry: RollupRegistry;
546
+ /** Options for {@link assertRollupParity}. */
547
+ export interface RollupParityOptions {
548
+ readonly onMismatch?: (message: string) => never;
549
+ readonly sql?: SqlCompileOptions;
550
+ }
551
+ /**
552
+ * The framework PARITY GUARD for grouped rollups (#468) — the GROUP-BY sibling of
553
+ * {@link assertReadModelParity}. For each input-set it materialises the rollup's managed VIEW (and any
554
+ * composed dependency VIEWs) over throwaway TEMP fixtures built from the leaf rows, reads the SQL-derived
555
+ * group rows, computes the TS `reduce` for the same inputs, and asserts they agree as a SET keyed by the
556
+ * group columns (GROUP BY output order is unspecified, so rows are matched by key, not position).
557
+ */
558
+ export declare function assertRollupParity(rollup: Rollup, db: ParityDb, sampleSets: RollupInputs[], options?: RollupParityOptions): void;