graphddb 0.2.5 → 0.3.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as SegmentedKey, q as SelectBuilderSpec, r as RawCondition, s as TransactionSpec, t as Manifest, u as RetryOverride, v as ExecutionPlan, w as ResolvedKey, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, x as CdcEmulatorOptions, y as ChangeHandler, z as Unsubscribe, F as FaultSpec, A as ConcurrentRecomputeRef, C as ChangeEvent, G as EventLog, H as ReplayOptions, J as ShardId, K as TransactionItemSpec, L as Param, N as ParamDescriptor, O as DefinitionMap, Q as OperationDefinition, f as DDBModel, M as ModelStatic, V as WriteDefinitionOptions, X as PartialQueryKeyOf, Y as StrictSelectSpec, Z as EntityInput, _ as UniqueQueryKeyOf, $ as EntityRef, a0 as ConditionInput, a1 as QueryModelContract, a2 as QueryMethodSpec, a3 as CommandModelContract, a4 as CommandMethodSpec, a5 as ContractSpec, a6 as QuerySpec, a7 as CommandSpec, a8 as ContextSpec, a9 as OperationsDocument, aa as AnyOperationDefinition, ab as BridgeBundle, ac as ConditionSpec } from './types--tdHK3vi.js';
2
- export { ad as BatchDeleteRequest, ae as BatchGetOptions, af as BatchGetRequest, ag as BatchGetResult, ah as BatchPutRequest, ai as BatchResult, aj as BatchWriteRequest, ak as CONTRACT_RANGE_FANOUT_CONCURRENCY, al as CdcMode, am as Change, an as ChangeBatch, ao as ChangeEventName, ap as ClockMode, aq as Column, ar as ColumnMap, as as CommandContractMethodSpec, at as CommandInputShape, au as CommandMethod, av as CommandPlan, aw as CommandResolutionTarget, ax as CommandResultKind, ay as CommandSelectShape, az as CompiledFragment, aA as ComposeSpec, aB as CondSlot, aC as ConditionCheckInput, aD as Connection, aE as ContractCallSignature, aF as ContractCardinality, aG as ContractCommandParams, aH as ContractCommandResult, aI as ContractComposeNode, aJ as ContractFromRef, aK as ContractInputArity, aL as ContractItem, aM as ContractKeyFieldRef, aN as ContractKeyInput, aO as ContractKeyRef, aP as ContractKeySpec, aQ as ContractKind, aR as ContractMethodOp, aS as ContractParamRef, aT as ContractQueryParams, aU as ContractResolution, aV as CtxBase, aW as DEFAULT_MAX_ATTEMPTS, aX as DEFAULT_RETRY_POLICY, aY as DeleteOptions, aZ as DeriveEffect, a_ as DerivedEdgeWrite, a$ as DerivedUpdate, b0 as DescriptorBinding, b1 as ENTITY_WRITES_MARKER, b2 as EdgeEffect, b3 as EffectPath, b4 as EmitEffect, b5 as EntityWritesDefinition, b6 as EntityWritesShape, b7 as ExecutableCommandContract, b8 as ExecutableQueryContract, b9 as FilterInput, ba as FilterSpec, bb as FragmentInput, bc as GsiDefinitionMarker, bd as GsiOptions, be as IdempotencyEffect, bf as InProcessWriteDescriptor, bg as InputArity, bh as KeyDefinitionMarker, bi as KeySegment, bj as KeySlot, bk as KeyStructure, bl as KeyedResult, bm as LIFECYCLE_CONTRACT_MARKER, bn as LifecycleContract, bo as LifecycleEffects, bp as LiteralParam, bq as ManifestEntity, br as ManifestField, bs as ManifestFieldType, bt as ManifestGsi, bu as ManifestKey, bv as ManifestRelation, bw as ManifestTable, bx as ModelRef, by as MutateMode, bz as MutateOptions, bA as MutateParallelResult, bB as MutateTransactionResult, bC as MutationBody, bD as MutationDescriptorMap, bE as MutationFragment, bF as MutationInputProxy, bG as MutationInputRef, bH as MutationIntent, bI as NumberParam, bJ as OperationKind, bK as OperationSpec, bL as ParallelOpResult, bM as ParamKind, bN as ParamSpec, bO as ParamStructure, bP as PersistCtx, bQ as PersistOrigin, bR as PlannedCommandMethod, bS as PutOptions, bT as QueryContractMethodSpec, bU as QueryEnvelopeResult, bV as QueryKeyOf, bW as QueryMethod, bX as QueryResult, bY as RangeConditionSpec, bZ as ReadEnvelope, b_ as ReadOpCtx, b$ as ReadOpKind, c0 as ReadOperationType, c1 as ReadRouteDescriptor, c2 as ReadRouteOptions, c3 as ReadRouteResult, c4 as RecordedCompose, c5 as RelationBuilder, c6 as RelationSelect, c7 as RelationSpec, c8 as RequiresEffect, c9 as Resolution, ca as RetryInfo, cb as RetryOperationKind, cc as SPEC_VERSION, cd as SegmentSpec, ce as SelectBuilder, cf as SelectOf, cg as StartingPosition, ch as StreamViewType, ci as StringParam, cj as TransactionContext, ck as TransactionItemType, cl as UniqueEffect, cm as Updatable, cn as UpdateOptions, co as WhenSpec, cp as WriteCtx, cq as WriteDescriptor, cr as WriteEnvelope, cs as WriteInput, ct as WriteKind, cu as WriteLifecyclePhase, cv as WriteMiddleware, cw as WriteOperationType, cx as WriteRecorder, cy as WriteResultProjection, cz as attachModelClass, cA as buildDeleteInput, cB as buildPutInput, cC as buildUpdateInput, cD as compileFragment, cE as compileMutationPlan, cF as compileSingleFragmentPlan, cG as cond, cH as contractOfMethodSpec, cI as definePlan, cJ as entityWrites, cK as executeBatchGet, cL as executeBatchWrite, cM as executeCommandMethod, cN as executeDelete, cO as executeKeyedBatchGet, cP as executePut, cQ as executeQueryMethod, cR as executeRangeFanout, cS as executeTransaction, cT as executeUpdate, cU as from, cV as getEntityWrites, cW as gsi, cX as isColumn, cY as isCommandModelContract, cZ as isCommandPlan, c_ as isContractComposeNode, c$ as isContractFromRef, d0 as isContractKeyFieldRef, d1 as isContractKeyRef, d2 as isContractParamRef, d3 as isEntityWritesDefinition, d4 as isKeySegment, d5 as isLifecycleContract, d6 as isMutationFragment, d7 as isMutationInputRef, d8 as isParam, d9 as isPlannedCommandMethod, da as isQueryModelContract, db as isRetryableError, dc as isRetryableTransactionCancellation, dd as k, de as key, df as lifecyclePhaseForIntent, dg as mintContractKeyFieldRef, dh as mintContractParamRef, di as mutation, dj as param, dk as publicCommandModel, dl as publicQueryModel, dm as query, dn as resolveLifecycle, dp as wholeKeysSentinel } from './types--tdHK3vi.js';
1
+ import { S as SelectableOf, g as PrimaryKeyOf, h as RequestContext, i as Middleware, j as ReadRequestKind, k as CtxModel, l as ReadParams, m as ReadRequestCtx, D as DynamoDBOperation, I as Item, E as Executor, n as RetryPolicy, o as ExecutionPlanSpec, p as EntityMetadata, K as KeyDefinition, G as GsiDefinition, F as FieldOptions, q as DynamoType, r as RelationOptions, A as AggregateOptions, s as AggregateValue, t as SelectBuilderSpec, u as RawCondition, v as TransactionSpec, w as Manifest, x as RetryOverride, y as ExecutionPlan, z as FieldMetadata, H as ResolvedKey, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, J as CdcEmulatorOptions, L as ChangeHandler, N as Unsubscribe, O as FaultSpec, Q as ConcurrentRecomputeRef, C as ChangeEvent, V as EventLog, X as ReplayOptions, Y as ShardId, Z as ViewDefinition, _ as ProjectionDefinition, $ as RelationMetadata, a0 as TransactionItemSpec, a1 as MaintainEffect, M as ModelStatic, f as DDBModel, a2 as Param, a3 as ParamDescriptor, a4 as DefinitionMap, a5 as OperationDefinition, a6 as WriteDefinitionOptions, a7 as PartialQueryKeyOf, a8 as StrictSelectSpec, a9 as EntityInput, aa as UniqueQueryKeyOf, ab as EntityRef, ac as ConditionInput, ad as QueryModelContract, ae as QueryMethodSpec, af as CommandModelContract, ag as CommandMethodSpec, ah as ContractSpec, ai as QuerySpec, aj as CommandSpec, ak as ContextSpec, al as OperationsDocument, am as AnyOperationDefinition, an as BridgeBundle, ao as ConditionSpec } from './types-DuJ08bgc.js';
2
+ export { ap as AggregateMetadata, aq as BatchDeleteRequest, ar as BatchGetOptions, as as BatchGetRequest, at as BatchGetResult, au as BatchPutRequest, av as BatchResult, aw as BatchWriteRequest, ax as CONTRACT_RANGE_FANOUT_CONCURRENCY, ay as CdcMode, az as Change, aA as ChangeBatch, aB as ChangeEventName, aC as ClockMode, aD as CollectionEffect, aE as CollectionOptions, aF as Column, aG as ColumnMap, aH as CommandContractMethodSpec, aI as CommandInputShape, aJ as CommandMethod, aK as CommandPlan, aL as CommandResolutionTarget, aM as CommandResultKind, aN as CommandSelectShape, aO as CompiledFragment, aP as ComposeSpec, aQ as CondSlot, aR as ConditionCheckInput, aS as Connection, aT as ContractCallSignature, aU as ContractCardinality, aV as ContractCommandParams, aW as ContractCommandResult, aX as ContractComposeNode, aY as ContractFromRef, aZ as ContractInputArity, a_ as ContractItem, a$ as ContractKeyFieldRef, b0 as ContractKeyInput, b1 as ContractKeyRef, b2 as ContractKeySpec, b3 as ContractKind, b4 as ContractMethodOp, b5 as ContractParamRef, b6 as ContractQueryParams, b7 as ContractResolution, b8 as CounterAggregate, b9 as CounterEffect, ba as CtxBase, bb as DEFAULT_MAX_ATTEMPTS, bc as DEFAULT_RETRY_POLICY, bd as DeleteOptions, be as DeriveEffect, bf as DerivedEdgeWrite, bg as DerivedUpdate, bh as DescriptorBinding, bi as ENTITY_WRITES_MARKER, bj as EdgeEffect, bk as EffectPath, bl as EmbeddedMetadata, bm as EmitEffect, bn as EntityWritesDefinition, bo as EntityWritesShape, bp as ExecutableCommandContract, bq as ExecutableQueryContract, br as FilterInput, bs as FilterSpec, bt as FragmentInput, bu as GsiDefinitionMarker, bv as GsiOptions, bw as IdempotencyEffect, bx as InProcessWriteDescriptor, by as InputArity, bz as KeyDefinitionMarker, bA as KeySegment, bB as KeySlot, bC as KeyStructure, bD as KeyedResult, bE as LIFECYCLE_CONTRACT_MARKER, bF as LifecycleContract, bG as LifecycleEffects, bH as LiteralParam, bI as MaintainConsistency, bJ as MaintainEvent, bK as MaintainItem, bL as MaintainTrigger, bM as MaintainUpdateMode, bN as MaintenanceGraph, bO as ManifestEntity, bP as ManifestField, bQ as ManifestFieldType, bR as ManifestGsi, bS as ManifestKey, bT as ManifestRelation, bU as ManifestTable, bV as MembershipEffect, bW as MembershipPredicate, bX as MembershipPredicateOp, bY as ModelRef, bZ as MutateMode, b_ as MutateOptions, b$ as MutateParallelResult, c0 as MutateTransactionResult, c1 as MutationBody, c2 as MutationDescriptorMap, c3 as MutationFragment, c4 as MutationInputProxy, c5 as MutationInputRef, c6 as MutationIntent, c7 as NumberParam, c8 as OperationKind, c9 as OperationSpec, ca as ParallelOpResult, cb as ParamKind, cc as ParamSpec, cd as ParamStructure, ce as PersistCtx, cf as PersistOrigin, cg as PlannedCommandMethod, ch as ProjectionDefinitionInput, ci as ProjectionDefinitionMap, cj as ProjectionMap, ck as ProjectionTransform, cl as ProjectionTransformOp, cm as ProjectionVia, cn as PutOptions, co as QueryContractMethodSpec, cp as QueryEnvelopeResult, cq as QueryKeyOf, cr as QueryMethod, cs as QueryResult, ct as RangeConditionSpec, cu as ReadEnvelope, cv as ReadOpCtx, cw as ReadOpKind, cx as ReadOperationType, cy as ReadRouteDescriptor, cz as ReadRouteOptions, cA as ReadRouteResult, cB as RecordedCompose, cC as RelationBuilder, cD as RelationConsistency, cE as RelationLimitOptions, cF as RelationPattern, cG as RelationProjection, cH as RelationReadOptions, cI as RelationSelect, cJ as RelationSpec, cK as RelationUpdateMode, cL as RelationWriteOptions, cM as RequiresEffect, cN as Resolution, cO as RetryInfo, cP as RetryOperationKind, cQ as SPEC_VERSION, cR as SegmentSpec, cS as SegmentedKey, cT as SelectBuilder, cU as SelectOf, cV as SnapshotEffect, cW as StartingPosition, cX as StreamViewType, cY as StringParam, cZ as TransactionContext, c_ as TransactionItemType, c$ as UniqueEffect, d0 as Updatable, d1 as UpdateOptions, d2 as VersionedDefinition, d3 as VersionedDefinitionInput, d4 as ViewDefinitionInput, d5 as ViewDefinitionMap, d6 as ViewRegistry, d7 as ViewSource, d8 as ViewSourceSlice, d9 as WhenSpec, da as WriteCtx, db as WriteDescriptor, dc as WriteEnvelope, dd as WriteInput, de as WriteKind, df as WriteLifecyclePhase, dg as WriteMiddleware, dh as WriteOperationType, di as WriteRecorder, dj as WriteResultProjection, dk as attachModelClass, dl as buildDeleteInput, dm as buildMaintenanceGraph, dn as buildPutInput, dp as buildUpdateInput, dq as compileFragment, dr as compileMutationPlan, ds as compileSingleFragmentPlan, dt as cond, du as contractOfMethodSpec, dv as definePlan, dw as defineProjection, dx as defineProjections, dy as defineVersioned, dz as defineView, dA as defineViews, dB as entityWrites, dC as executeBatchGet, dD as executeBatchWrite, dE as executeCommandMethod, dF as executeDelete, dG as executeKeyedBatchGet, dH as executePut, dI as executeQueryMethod, dJ as executeRangeFanout, dK as executeTransaction, dL as executeUpdate, dM as from, dN as getEntityWrites, dO as gsi, dP as identity, dQ as isColumn, dR as isCommandModelContract, dS as isCommandPlan, dT as isContractComposeNode, dU as isContractFromRef, dV as isContractKeyFieldRef, dW as isContractKeyRef, dX as isContractParamRef, dY as isEntityWritesDefinition, dZ as isKeySegment, d_ as isLifecycleContract, d$ as isMaintainTrigger, e0 as isMutationFragment, e1 as isMutationInputRef, e2 as isParam, e3 as isPlannedCommandMethod, e4 as isProjectionDefinition, e5 as isQueryModelContract, e6 as isRetryableError, e7 as isRetryableTransactionCancellation, e8 as isViewDefinition, e9 as k, ea as key, eb as lifecyclePhaseForIntent, ec as maintainTrigger, ed as mintContractKeyFieldRef, ee as mintContractParamRef, ef as mutation, eg as param, eh as preview, ei as publicCommandModel, ej as publicQueryModel, ek as query, el as resolveLifecycle, em as whenMember, en as wholeKeysSentinel } from './types-DuJ08bgc.js';
3
3
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4
4
  import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
5
5
 
@@ -34,7 +34,7 @@ type ListOptions<T, S extends SelectableOf<T> = SelectableOf<T>> = {
34
34
 
35
35
  /**
36
36
  * Read-middleware runtime (issue #138). Drives the registered {@link Middleware}
37
- * list through the read hook points R1–R5 with the ordering the proposal
37
+ * list through the read hook points R1–R5 with the ordering docs/middleware.md
38
38
  * specifies:
39
39
  *
40
40
  * - `before*` (R1 / R2) run **first-registered-first** (FIFO);
@@ -52,7 +52,7 @@ type ListOptions<T, S extends SelectableOf<T> = SelectableOf<T>> = {
52
52
  * set stable even if middleware is (un)registered mid-flight, and is
53
53
  * concurrency-safe under parallel fan-out (#31).
54
54
  *
55
- * @see docs/proposals/read-middleware.md
55
+ * @see docs/middleware.md
56
56
  */
57
57
 
58
58
  /**
@@ -313,71 +313,6 @@ interface RelationExecutionPlan {
313
313
  readonly resultPaths: readonly string[];
314
314
  }
315
315
 
316
- type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
317
- interface FieldOptions {
318
- format?: 'datetime' | 'date';
319
- readonly?: boolean;
320
- serialize?: (value: unknown) => unknown;
321
- deserialize?: (value: unknown) => unknown;
322
- }
323
- interface FieldMetadata {
324
- propertyName: string;
325
- dynamoType: DynamoType;
326
- options?: FieldOptions;
327
- /**
328
- * For a `@literal(...)` field — a `string`-stored field whose value is one of a
329
- * known, finite set — the allowed literal values, in declaration order (issue
330
- * #75). Recorded purely as model metadata so a Contract param bound into the
331
- * field can recover the precise `literal` param-kind from its bind position
332
- * (`src/spec/contracts.ts`), serializing as `{ type: 'literal', literals: [...] }`
333
- * instead of the imprecise `'string'`. JSON-safe by construction (a non-empty
334
- * array of strings). `undefined` for every other field decorator (`@string`,
335
- * `@number`, …), so plain fields are byte-for-byte unchanged (backward compat).
336
- */
337
- literals?: readonly string[];
338
- }
339
-
340
- interface KeyDefinition {
341
- /** The canonical structured key (PK/SK segment lists). */
342
- segmented: SegmentedKey;
343
- inputFieldNames: string[];
344
- }
345
- interface GsiDefinition {
346
- indexName: string;
347
- /** The canonical structured key (PK/SK segment lists). */
348
- segmented: SegmentedKey;
349
- inputFieldNames: string[];
350
- unique: boolean;
351
- }
352
- interface RelationLimitOptions {
353
- default: number;
354
- max: number;
355
- }
356
- interface RelationOptions {
357
- limit?: RelationLimitOptions;
358
- order?: 'ASC' | 'DESC';
359
- }
360
- interface RelationMetadata {
361
- type: 'hasMany' | 'hasOne' | 'belongsTo';
362
- propertyName: string;
363
- targetFactory: () => new (...args: unknown[]) => unknown;
364
- keyBinding: Record<string, string>;
365
- options?: RelationOptions;
366
- }
367
- interface EmbeddedMetadata {
368
- propertyName: string;
369
- modelFactory: () => new (...args: unknown[]) => unknown;
370
- }
371
- interface EntityMetadata {
372
- tableName: string;
373
- prefix: string;
374
- fields: FieldMetadata[];
375
- primaryKey: KeyDefinition | null;
376
- gsiDefinitions: GsiDefinition[];
377
- relations: RelationMetadata[];
378
- embeddedFields: EmbeddedMetadata[];
379
- }
380
-
381
316
  interface LintRule {
382
317
  id: string;
383
318
  severity: 'error' | 'warning';
@@ -401,6 +336,22 @@ declare class Linter {
401
336
  declare class MetadataRegistry {
402
337
  private static store;
403
338
  private static _linter;
339
+ /**
340
+ * A monotonically-increasing counter bumped on every change to the set of
341
+ * registered models ({@link register} / {@link clear}). It lets a consumer that
342
+ * caches a structure derived from the whole registry (e.g. the maintenance graph
343
+ * in `src/spec/mutation-command.ts`) detect that the registry has changed since
344
+ * the cache was built and rebuild — closing the silent-drop window where a model
345
+ * registered after a cache build would be invisible to it. Only the membership
346
+ * of {@link store} changes here; per-model lazy finalize ({@link finalize}) does
347
+ * not bump it (it adds no relations/triggers, so it cannot change the graph).
348
+ */
349
+ private static _generation;
350
+ /**
351
+ * The current registry generation (see {@link _generation}). A consumer caches
352
+ * the value it built against and rebuilds when it no longer matches.
353
+ */
354
+ static get generation(): number;
404
355
  static register(target: Function, metadata: EntityMetadata): void;
405
356
  static get(target: Function): EntityMetadata;
406
357
  static has(target: Function): boolean;
@@ -531,9 +482,92 @@ declare const map: FieldDecorator<Record<string, unknown>>;
531
482
  declare function embedded(modelFactory: () => new (...args: unknown[]) => unknown): (_value: undefined, context: ClassFieldDecoratorContext) => void;
532
483
 
533
484
  declare function hasMany(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
534
- declare function belongsTo(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>): (_value: undefined, context: ClassFieldDecoratorContext) => void;
485
+ declare function belongsTo(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
535
486
  declare function hasOne(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>): (_value: undefined, context: ClassFieldDecoratorContext) => void;
536
487
 
488
+ /**
489
+ * `@aggregate` — a **scalar-aggregate** field decorator (Epic #118 §5.2 counter /
490
+ * latest preset; issue #122).
491
+ *
492
+ * Where `@hasMany` / `@belongsTo` declare a *navigation* (a relation yielding
493
+ * model instances) and `@string` / `@number` declare a *stored* attribute,
494
+ * `@aggregate` declares a **scalar value derived by aggregating a source
495
+ * entity's rows**. It is a field (the decorated property holds a `number` /
496
+ * `string`), but — like a relation — it carries a source `targetFactory` +
497
+ * `keyBinding` selecting the rows it aggregates. The declaration surface mirrors
498
+ * the RFC §5.2 example:
499
+ *
500
+ * ```ts
501
+ * @model({ table: 'AppTable', prefix: 'THREAD' })
502
+ * class ThreadSummaryModel extends DDBModel {
503
+ * @string threadId!: string;
504
+ *
505
+ * @aggregate(() => PostModel, { threadId: 'threadId' }, {
506
+ * pattern: 'counter', value: count(),
507
+ * write: { maintainedOn: ['Post.created', 'Post.removed'] },
508
+ * })
509
+ * postCount!: number;
510
+ *
511
+ * @aggregate(() => PostModel, { threadId: 'threadId' }, {
512
+ * pattern: 'counter', value: max('createdAt'),
513
+ * write: { maintainedOn: ['Post.created'] },
514
+ * })
515
+ * lastPostAt!: string;
516
+ * }
517
+ * ```
518
+ *
519
+ * ## Scope (issue #122)
520
+ *
521
+ * This is the **declaration surface only** — the decorator records an
522
+ * {@link AggregateMetadata} into its own collector array (drained by `@model`
523
+ * into `EntityMetadata.aggregates`), exactly as the relation decorators push to
524
+ * the relation collector. The aggregation *effect* (maintenance graph #124,
525
+ * compile injection #125, runtime #127) is out of scope and consumes this
526
+ * metadata later.
527
+ *
528
+ * ## Vocabulary reuse (no double definition)
529
+ *
530
+ * `options.pattern` is the shared {@link RelationPattern} (typically `'counter'`)
531
+ * and `options.write` is the shared {@link RelationWriteOptions} — `maintainedOn`
532
+ * triggers on the `created|updated|removed` event vocabulary plus
533
+ * `consistency` (`transactional|eventual`) / `updateMode` (`mutation|stream`).
534
+ * Both were defined by B (#121) in `src/metadata/types.ts` and are **reused**
535
+ * here, never re-defined.
536
+ *
537
+ * @param targetFactory - Resolves the source entity whose rows are aggregated
538
+ * (lazy, mirroring the relation decorators, to tolerate circular imports).
539
+ * @param keyBinding - The key binding selecting the source rows
540
+ * (target key field → source field).
541
+ * @param options - The aggregate preset / value / maintenance-write declaration.
542
+ */
543
+ declare function aggregate(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options: AggregateOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
544
+ /**
545
+ * Value helper for an `@aggregate` field: the **count** aggregation — the
546
+ * cardinality of source rows matched by the key binding (Epic #118 §5.2
547
+ * `counter` preset, `count()`). It takes no source field (it counts rows, not a
548
+ * value), so the decorated field is a `number` (e.g. `postCount!: number`).
549
+ *
550
+ * Produces the {@link AggregateValue} IR node `{ op: 'count' }`. This is the
551
+ * standalone, recorder-free authoring helper used directly in the decorator
552
+ * argument (mirroring the standalone `preview(...)` / `identity(...)` projection
553
+ * helpers A (#120) exposes for relation options).
554
+ */
555
+ declare function count(): AggregateValue;
556
+ /**
557
+ * Value helper for an `@aggregate` field: the **max** aggregation — the maximum
558
+ * value of a named source `field` across the rows matched by the key binding
559
+ * (Epic #118 §5.2 `counter` / latest preset, `max('createdAt')`). The decorated
560
+ * field's type matches the source field's (e.g. `lastPostAt!: string` ←
561
+ * `max('createdAt')`).
562
+ *
563
+ * Produces the {@link AggregateValue} IR node `{ op: 'max', field }`.
564
+ *
565
+ * @param field - The source field whose maximum is tracked (e.g. `'createdAt'`).
566
+ * @throws if `field` is not a non-empty string (an aggregation over no source
567
+ * field is meaningless — a defensive runtime check mirroring `@literal`'s).
568
+ */
569
+ declare function max(field: string): AggregateValue;
570
+
537
571
  declare class TableMapping {
538
572
  private static mapping;
539
573
  static set(mapping: Record<string, string>): void;
@@ -1041,6 +1075,246 @@ declare class CdcEmulator {
1041
1075
  /** Create a CDC emulator (spec §10). */
1042
1076
  declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
1043
1077
 
1078
+ /**
1079
+ * CDC **maintenance drain** (issue #130) — the asynchronous lower for
1080
+ * `updateMode: 'stream'` maintainers (Epic #118 §5.1).
1081
+ *
1082
+ * A synchronous (`updateMode: 'mutation'`) maintainer composes its owner-row write
1083
+ * into the SAME atomic `TransactWriteItems` as the source write (#127). A
1084
+ * `updateMode: 'stream'` maintainer instead emits a **maintenance-outbox** marker row
1085
+ * (`OUTBOX#MAINT#…`) ATOMICALLY with the source write (the #130 compile lowering in
1086
+ * `src/spec/mutation-command.ts`), and THIS consumer — driven by the {@link
1087
+ * import('./emulator.js').CdcEmulator} (or, in production, real DynamoDB Streams +
1088
+ * a Lambda built around {@link createMaintenanceDrainHandler}) — applies the owner-row
1089
+ * write asynchronously.
1090
+ *
1091
+ * ## Why a separate async path exists (not just "do it later synchronously")
1092
+ *
1093
+ * The async path realizes the maintenance operations a single synchronous
1094
+ * `UpdateExpression` cannot express against an unread item:
1095
+ *
1096
+ * - **collection `maxItems` trim** — keeping a bounded, ordered list requires reading
1097
+ * the current list, appending, sorting, and trimming (a read-modify-write); a
1098
+ * synchronous `list_append` can only append (Phase 1 was append-only, #127).
1099
+ * - **`removed`-driven splice** — removing an entry from a maintained collection on a
1100
+ * source `removed` event likewise needs the current list.
1101
+ * - **running `max(field)`** — a conditional `SET` (`#a = :v` guarded by
1102
+ * `attribute_not_exists(#a) OR #a < :v`) whose failed guard, if run synchronously,
1103
+ * would roll back the legitimate source write.
1104
+ *
1105
+ * ## Delivery guarantees (reused from the CDC substrate)
1106
+ *
1107
+ * The maintenance-outbox row is recorded ATOMICALLY with the source write, so the
1108
+ * intent can never be lost ("wrote the row, lost the maintenance" is impossible). The
1109
+ * CDC substrate then delivers it **at-least-once**, **per-shard ordered** (a shard is
1110
+ * `hash(pk)`, and the outbox row's `pk` is keyed off the SOURCE row's key, so all
1111
+ * maintenance events for one source entity land on one shard in source-write order),
1112
+ * with a **DLQ** for poison events. This consumer is therefore written to be
1113
+ * **idempotent / commutative** wherever at-least-once redelivery could double-apply:
1114
+ *
1115
+ * - a `snapshot` `SET` is naturally idempotent (re-applying the same projection is a
1116
+ * no-op);
1117
+ * - a `collection` rebuild de-duplicates by the projected item's identity key, so a
1118
+ * redelivered append does not grow the list twice;
1119
+ * - a `max` conditional `SET` is idempotent (the guard rejects a non-greater value);
1120
+ * - a `count` `ADD` is the ONE non-idempotent op — but the source-row create guard
1121
+ * (`attribute_not_exists`) means a given source `created` event is emitted at most
1122
+ * once into the outbox, and the per-shard checkpoint advances only over acked events,
1123
+ * so a `count` is applied once per genuine source lifecycle. (A duplicate DELIVERY of
1124
+ * the same event is folded by the consumer's per-event de-dup set within a drain.)
1125
+ */
1126
+
1127
+ /**
1128
+ * The PK prefix a maintenance-outbox row keys on — the single source of truth shared
1129
+ * with the compiler ({@link import('../spec/mutation-command.js')}'s
1130
+ * `MAINT_OUTBOX_PK_PREFIX`). The drain selects its events by `keys.pk.startsWith` of
1131
+ * this. Kept as a local literal (rather than importing a non-exported compiler const)
1132
+ * so the cdc module does not depend on the spec module; the value is asserted equal by
1133
+ * the integration tests that round-trip a real stream maintainer through both.
1134
+ */
1135
+ declare const MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
1136
+ /** Options for {@link createMaintenanceDrain}. */
1137
+ interface MaintenanceDrainOptions {
1138
+ /**
1139
+ * The model classes (or `ModelStatic`s) whose stream maintainers this drain
1140
+ * applies. The drain builds a scoped {@link MaintenanceGraph} over them to resolve
1141
+ * each maintenance-outbox event back to its declared effect. Defaults to the GLOBAL
1142
+ * registry when omitted.
1143
+ */
1144
+ readonly models?: readonly Function[];
1145
+ /**
1146
+ * The {@link ViewDefinition}s (`defineView`) whose maintainers this drain applies
1147
+ * (issue #132). A view's destination is its dedicated view model; its source
1148
+ * classes should also be in {@link models} so the drain can resolve their
1149
+ * maintenance-outbox events. Defaults to none.
1150
+ */
1151
+ readonly views?: readonly ViewDefinition[];
1152
+ }
1153
+ /**
1154
+ * A maintenance drain: a CDC consumer ({@link ChangeHandler}) that applies the
1155
+ * owner-row writes for `updateMode: 'stream'` maintainers. Construct via
1156
+ * {@link createMaintenanceDrain}, then `subscribe` its {@link handler} to a
1157
+ * {@link import('./emulator.js').CdcEmulator} (dev/test) or wire it into a real
1158
+ * Streams Lambda (production).
1159
+ */
1160
+ declare class MaintenanceDrain {
1161
+ private readonly graph;
1162
+ /** Owner entity name → its model class (for the owner-row key derivation). */
1163
+ private readonly ownerByName;
1164
+ /** Per-event-id de-dup within a single handler invocation (at-least-once). */
1165
+ private appliedSeq;
1166
+ /** Count of owner-row writes applied (test/diagnostic). */
1167
+ private applied;
1168
+ constructor(opts?: MaintenanceDrainOptions);
1169
+ /** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
1170
+ readonly handler: ChangeHandler;
1171
+ /** Resolve one maintenance-outbox event to its effect and apply the owner-row write. */
1172
+ private applyOne;
1173
+ /** Resolve a maintenance-outbox event back to the declared {@link MaintainItem}. */
1174
+ private resolve;
1175
+ /** A `snapshot` SET of each projected attribute onto the owner row (idempotent). */
1176
+ private applySnapshot;
1177
+ /**
1178
+ * A sparse-view **membership** write (#133): evaluate the membership predicate against
1179
+ * the source image; PUT the view row (its projection + key fields) when the predicate
1180
+ * holds, DELETE it when it flips false. A source `removed` event always deletes (the
1181
+ * source no longer exists, so its view row must disappear regardless of the predicate).
1182
+ *
1183
+ * Both ops are idempotent under at-least-once redelivery: a repeated PUT writes the same
1184
+ * row, a repeated DELETE is a no-op. The view row is keyed by the source identity
1185
+ * (`effect.keys`), so the PUT/DELETE always targets the SAME physical row the predicate
1186
+ * gates.
1187
+ */
1188
+ private applyMembership;
1189
+ /**
1190
+ * A running `max`: read the owner row, and `SET` the attribute to the source value
1191
+ * only when it is greater (or absent). Implemented as a conditional update so a
1192
+ * redelivered / out-of-order older value never regresses the stored max — idempotent
1193
+ * and commutative under at-least-once delivery.
1194
+ */
1195
+ private applyMax;
1196
+ /**
1197
+ * A bounded `collection`: read the current list, apply the event (append for a
1198
+ * `created`/`updated` source, splice for a `removed`), de-duplicate by the projected
1199
+ * identity key, order by `orderBy` (in `orderDir`, default DESC), trim to `maxItems`, and write the whole
1200
+ * list back. The read-modify-write is exactly what a single synchronous
1201
+ * `UpdateExpression` cannot do — the reason a bounded/ordered collection is a stream
1202
+ * maintainer (Phase 1 sync was append-only).
1203
+ */
1204
+ private applyCollection;
1205
+ /** Owner-row writes applied so far (test/diagnostic). */
1206
+ appliedCount(): number;
1207
+ /** Clear the per-delivery de-dup set + counters (test reset). */
1208
+ reset(): void;
1209
+ }
1210
+ /** Create a {@link MaintenanceDrain} (issue #130). */
1211
+ declare function createMaintenanceDrain(opts?: MaintenanceDrainOptions): MaintenanceDrain;
1212
+ /**
1213
+ * Create just the CDC {@link ChangeHandler} for a maintenance drain — the form to
1214
+ * subscribe to an emulator or wire into a production Streams Lambda. A thin wrapper
1215
+ * over {@link createMaintenanceDrain} for callers that only need the handler.
1216
+ */
1217
+ declare function createMaintenanceDrainHandler(opts?: MaintenanceDrainOptions): ChangeHandler;
1218
+
1219
+ /**
1220
+ * External-projection **sink drain** (issue #132) — the asynchronous runtime for
1221
+ * `defineProjection` (`externalProjection`, Epic #118 §5.3 / RFC §4.A.8).
1222
+ *
1223
+ * A `defineProjection` declares a stream + idempotency projection of a source
1224
+ * entity into an EXTERNAL sink (BigQuery / S3 / OpenSearch / Neptune in
1225
+ * production). The transport is the CDC stream: the source entity's writes surface
1226
+ * as {@link ChangeEvent}s (real DynamoDB Streams in production, the
1227
+ * {@link import('./emulator.js').CdcEmulator} in dev/test), and THIS consumer
1228
+ * projects each event's source image and upserts it into the sink.
1229
+ *
1230
+ * ## At-least-once → exactly-once-effect via the idempotency key
1231
+ *
1232
+ * The CDC substrate delivers at-least-once (a single source event may be delivered
1233
+ * more than once). An external sink upsert is the ONE place a duplicate would
1234
+ * double-write, so the projection folds redelivery by the declared
1235
+ * `idempotencyKey` (a source attribute, e.g. `resultId`): the sink is an UPSERT
1236
+ * keyed by that value, so re-delivering the same source event overwrites the same
1237
+ * sink record rather than appending a second. This is the standard "make
1238
+ * at-least-once idempotent with a business key" pattern; the sink need only
1239
+ * support an idempotent put keyed by the idempotency key.
1240
+ *
1241
+ * ## The sink abstraction (dev/test vs production)
1242
+ *
1243
+ * {@link ProjectionSink} is the minimal contract a real sink adapter implements
1244
+ * (`upsert(key, record)` / `delete(key)`). Production wires a BigQuery / S3 /
1245
+ * OpenSearch adapter; dev/test uses {@link InMemoryProjectionSink}, an in-process
1246
+ * map that the integration tests assert convergence + idempotency against. The
1247
+ * drain itself is sink-agnostic — it resolves the projection, computes the key,
1248
+ * and calls the adapter.
1249
+ */
1250
+
1251
+ /**
1252
+ * The minimal contract a projection sink adapter implements. An upsert is keyed by
1253
+ * the idempotency key (so a redelivery overwrites the same record), a delete
1254
+ * removes it (for a source `removed` event projected to the sink).
1255
+ */
1256
+ interface ProjectionSink {
1257
+ /** Upsert the projected record under the idempotency key (idempotent). */
1258
+ upsert(key: string, record: Record<string, unknown>): void | Promise<void>;
1259
+ /** Remove the record under the idempotency key (for a projected `removed`). */
1260
+ delete?(key: string): void | Promise<void>;
1261
+ }
1262
+ /**
1263
+ * An in-process {@link ProjectionSink} for dev/test — a map keyed by the
1264
+ * idempotency key. Mirrors what a real sink (BigQuery / S3) does logically: an
1265
+ * upsert keyed by a business key folds at-least-once redelivery to one record. The
1266
+ * integration tests assert convergence + idempotency against {@link records}.
1267
+ */
1268
+ declare class InMemoryProjectionSink implements ProjectionSink {
1269
+ /** The current sink contents, keyed by the idempotency key. */
1270
+ readonly records: Map<string, Record<string, unknown>>;
1271
+ /** Total upsert CALLS (including redelivered ones) — distinguishes calls from records. */
1272
+ private upserts;
1273
+ upsert(key: string, record: Record<string, unknown>): void;
1274
+ delete(key: string): void;
1275
+ /** The number of distinct sink records (the converged projection size). */
1276
+ size(): number;
1277
+ /** The total upsert calls (≥ size when redelivery occurred). */
1278
+ upsertCount(): number;
1279
+ /** Reset the sink (test). */
1280
+ reset(): void;
1281
+ }
1282
+ /** Options for {@link createProjectionSinkDrain}. */
1283
+ interface ProjectionSinkDrainOptions {
1284
+ /** The {@link ProjectionDefinition}s (`defineProjection`) this drain applies. */
1285
+ readonly projections: readonly ProjectionDefinition[];
1286
+ /** The external sink adapter the projected records are upserted into. */
1287
+ readonly sink: ProjectionSink;
1288
+ }
1289
+ /**
1290
+ * A projection sink drain (issue #132): a CDC consumer ({@link ChangeHandler}) that
1291
+ * projects each source event into the external sink, folding at-least-once delivery
1292
+ * by the projection's idempotency key. Construct via
1293
+ * {@link createProjectionSinkDrain}, then subscribe its {@link handler} to a
1294
+ * {@link import('./emulator.js').CdcEmulator} (dev/test) or a real Streams Lambda.
1295
+ */
1296
+ declare class ProjectionSinkDrain {
1297
+ private readonly sink;
1298
+ /** Source-entity CLASS name (the `event.model` the capture seam records) → projections. */
1299
+ private readonly bySource;
1300
+ /** Per-(shard,seq) de-dup within one handler invocation (at-least-once). */
1301
+ private applied;
1302
+ constructor(opts: ProjectionSinkDrainOptions);
1303
+ /** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
1304
+ readonly handler: ChangeHandler;
1305
+ /** Project one source event into the sink for every matching projection. */
1306
+ private applyOne;
1307
+ /** Clear the per-delivery de-dup set (test reset). */
1308
+ reset(): void;
1309
+ }
1310
+ /** Create a {@link ProjectionSinkDrain} (issue #132). */
1311
+ declare function createProjectionSinkDrain(opts: ProjectionSinkDrainOptions): ProjectionSinkDrain;
1312
+ /**
1313
+ * Create just the CDC {@link ChangeHandler} for a projection sink drain — the form
1314
+ * to subscribe to an emulator or wire into a production Streams Lambda.
1315
+ */
1316
+ declare function createProjectionSinkHandler(opts: ProjectionSinkDrainOptions): ChangeHandler;
1317
+
1044
1318
  /**
1045
1319
  * Per-key cursor envelope for batched `range` (`list`) contract methods (issue
1046
1320
  * #62, CQRS single-service runtime; spec `docs/cqrs-contract.md`,
@@ -1125,10 +1399,14 @@ declare function decodePerKeyCursor(cursor: string, expectedKey: Record<string,
1125
1399
  /**
1126
1400
  * Creates a Linter pre-loaded with rules safe for Entity registration.
1127
1401
  *
1128
- * Rules included: no-scan, require-limit, gsi-ambiguity, missing-gsi, relation-depth.
1129
- * query-boundary is excluded because it flags non-unique GSIs which
1130
- * are valid for list() operations; boundary enforcement is handled
1131
- * at runtime by the planner's boundary-check.
1402
+ * Rules included: no-scan, require-limit, gsi-ambiguity, missing-gsi,
1403
+ * relation-depth, same-partition-preset, plus the Epic #118 Phase 1 maintenance
1404
+ * validators (issue #126): missing-context, 400kb, hot-partition, fan-out,
1405
+ * multi-maintainer-same-row. The maintenance validators are no-ops for any
1406
+ * relation that does not declare `write.maintainedOn`, so unannotated models are
1407
+ * unaffected. query-boundary is excluded because it flags non-unique GSIs which
1408
+ * are valid for list() operations; boundary enforcement is handled at runtime by
1409
+ * the planner's boundary-check.
1132
1410
  */
1133
1411
  declare function createDefaultLinter(): Linter;
1134
1412
 
@@ -1302,12 +1580,42 @@ type EdgeLifecycle = 'onCreate' | 'onDelete' | 'onUpdateKeyChange';
1302
1580
  * lifecycle-agnostic — the *same* edge is created on create, deleted on delete,
1303
1581
  * and moved on a foreign-key change; {@link deriveEdgeWriteItems} fans it out
1304
1582
  * into the three lifecycle forms.
1583
+ *
1584
+ * ## `inverse` — dual-edge synchronization (Epic #118 pattern 9, issue #133)
1585
+ *
1586
+ * The plain edge above is **one physical row** that serves *both* read directions:
1587
+ * the base PK reads one direction and an inverse GSI (auto-maintained by DynamoDB)
1588
+ * reads the other, so a bidirectional relation costs a single write. The RFC §1
1589
+ * pattern-9 gap is the case where the inverse direction is NOT served by that GSI
1590
+ * but by a **second physical row** on its own partition — the genuine *dual edge*
1591
+ * (forward + inverse rows). {@link inverse}, when present, names a SECOND adjacency
1592
+ * model + relation property whose row is the inverse edge; {@link
1593
+ * deriveEdgeWriteItemsFor} then maintains BOTH rows from this single (one-direction)
1594
+ * declaration — the create `Put`s both, the delete `Delete`s both, a foreign-key
1595
+ * change `Delete`s+`Put`s both — so the two directions can never drift. Omit
1596
+ * `inverse` for the historical single-row-plus-GSI bidirectional edge.
1305
1597
  */
1306
1598
  interface EdgeWriteDeclaration {
1307
1599
  /** Resolves the target model whose relation this edge feeds (e.g. `GroupModel`). */
1308
1600
  readonly targetFactory: () => new (...args: unknown[]) => unknown;
1309
1601
  /** The relation property on the target that reads this edge (e.g. `members`). */
1310
1602
  readonly relationProperty: string;
1603
+ /**
1604
+ * The INVERSE edge to keep in sync (Epic #118 pattern 9 / issue #133, dual-edge).
1605
+ * Names the second adjacency model whose own row materializes the reverse-direction
1606
+ * relation. When present, every lifecycle derivation maintains the inverse row
1607
+ * alongside the forward row in the SAME transaction, so a one-direction declaration
1608
+ * keeps a two-row bidirectional edge consistent. Absent = the historical single-row
1609
+ * (base + inverse GSI) bidirectional edge.
1610
+ */
1611
+ readonly inverse?: {
1612
+ /** Resolves the inverse adjacency model whose row IS the inverse edge. */
1613
+ readonly adjacencyFactory: () => new (...args: unknown[]) => unknown;
1614
+ /** Resolves the target model whose relation the inverse row feeds. */
1615
+ readonly targetFactory: () => new (...args: unknown[]) => unknown;
1616
+ /** The relation property on the inverse target that reads the inverse row. */
1617
+ readonly relationProperty: string;
1618
+ };
1311
1619
  }
1312
1620
  /**
1313
1621
  * Marker carried by a model's static `writes` member ({@link edgeWrites}), so it
@@ -1348,6 +1656,34 @@ interface EdgeWriteRecorder {
1348
1656
  * `relationProperty` relation (→ adjacency `Delete`(old) + `Put`(new)).
1349
1657
  */
1350
1658
  updateKeyChangeEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeWriteDeclaration;
1659
+ /**
1660
+ * Declare a **dual edge** (Epic #118 pattern 9 / issue #133): a bidirectional edge
1661
+ * maintained as TWO physical rows — a forward row on THIS adjacency entity (read by
1662
+ * `forward.target.relationProperty`) and an inverse row on a SECOND adjacency model
1663
+ * (read by `inverse.target.relationProperty`). Both rows are written / deleted /
1664
+ * moved together from this single declaration, so the two directions stay in sync
1665
+ * without an inverse GSI. Use this when the reverse direction lives on its own
1666
+ * partition (a second row) rather than a GSI of the forward row.
1667
+ *
1668
+ * @example
1669
+ * ```ts
1670
+ * // On the forward adjacency entity (FollowEdge: FOLLOWER#a / FOLLOWING#b):
1671
+ * static readonly writes = edgeWrites((w) => [
1672
+ * w.dualEdge(
1673
+ * { target: () => UserModel, relationProperty: 'following' },
1674
+ * { adjacency: () => FollowedByEdgeModel, target: () => UserModel, relationProperty: 'followers' },
1675
+ * ),
1676
+ * ]);
1677
+ * ```
1678
+ */
1679
+ dualEdge(forward: {
1680
+ target: () => new (...args: unknown[]) => unknown;
1681
+ relationProperty: string;
1682
+ }, inverse: {
1683
+ adjacency: () => new (...args: unknown[]) => unknown;
1684
+ target: () => new (...args: unknown[]) => unknown;
1685
+ relationProperty: string;
1686
+ }): EdgeWriteDeclaration;
1351
1687
  }
1352
1688
  /**
1353
1689
  * Declare an adjacency entity's edge write side (issue #82), as the model's
@@ -1419,6 +1755,182 @@ declare function getEdgeWrites(modelClass: new (...args: unknown[]) => unknown):
1419
1755
  */
1420
1756
  declare function deriveModelEdgeWriteItems(modelClass: new (...args: unknown[]) => unknown, lifecycle: EdgeLifecycle): TransactionItemSpec[];
1421
1757
 
1758
+ /**
1759
+ * Maintenance **repair / rebuild planner** (Epic #118 Phase 2, issue #131).
1760
+ *
1761
+ * ## What this is
1762
+ *
1763
+ * The async drain ({@link import('../cdc/maintenance-drain.js')}, #130) keeps a
1764
+ * maintained owner row (a snapshot / collection / counter) in sync INCREMENTALLY —
1765
+ * it applies one source event at a time. Over a long-lived table the materialized
1766
+ * value can nevertheless **drift** from the source of truth: a dropped / dead-lettered
1767
+ * stream event, a maintainer added AFTER source rows already existed (initial
1768
+ * un-built state), a bug in an earlier maintainer version, or an out-of-band write.
1769
+ *
1770
+ * The rebuild planner is the repair for that drift. For a given owner row × maintainer
1771
+ * it **re-derives the owner-row value from scratch by scanning every source row that
1772
+ * belongs to that owner**, using the SAME projection / aggregation / ordering rules the
1773
+ * drain uses (shared via `maintenance-projection.ts` and this module), then:
1774
+ *
1775
+ * - **{@link detectDrift}** — compares the re-derived value against the value currently
1776
+ * stored on the owner row and reports whether (and how) they differ; or
1777
+ * - **{@link rebuild}** — writes the re-derived ABSOLUTE value onto the owner row, so the
1778
+ * materialized data converges to exactly what the source says.
1779
+ *
1780
+ * After a rebuild the owner row equals what an incremental drain would have converged to
1781
+ * had it never missed an event — the AC: "rebuild で materialized が source と一致する".
1782
+ *
1783
+ * ## Granularity: per owner row × maintainer
1784
+ *
1785
+ * Drift is detected and repaired at the **owner-row × maintainer** grain — the natural
1786
+ * unit, because one owner row's materialized value is a pure function of the set of
1787
+ * source rows bound to it. The caller names the owner rows to repair (their key inputs);
1788
+ * the planner discovers the source rows for each by querying the source on the maintenance
1789
+ * key binding (the same binding the live `hasMany` / `@aggregate` read uses, inverted:
1790
+ * the owner row supplies the binding value, the source partition is scanned for matches).
1791
+ *
1792
+ * ## count is re-derived as an ABSOLUTE SET, not an ADD
1793
+ *
1794
+ * The maintained counter applies a signed `ADD` per event (#141 / #130). A rebuild
1795
+ * instead **counts the source rows and SETs the absolute value** — it does not trust the
1796
+ * accumulated counter, it recomputes it. This is the whole point of repair (an ADD that
1797
+ * has drifted cannot be corrected by another ADD). The `max(field)` counter is re-derived
1798
+ * as the maximum over the scanned source rows and SET unconditionally (the rebuild is the
1799
+ * authority, so it does not use the drain's advance-only conditional guard).
1800
+ *
1801
+ * ## Execution form (production positioning)
1802
+ *
1803
+ * Like the drain applier, this is a **TS-side planner/executor** intended for dev/test and
1804
+ * **operational repair tooling** — run on-demand (a "repair this thread's summary now"
1805
+ * admin action) or as a periodic batch (a scheduled reconciliation sweep over a set of
1806
+ * owner rows). It is NOT injected into the SSoT spec and emits no generated artifact: a
1807
+ * rebuild is a host-side reconciliation against live DynamoDB, not part of a model's
1808
+ * compiled write contract — so there is no Python mirror (the Python runtime executes the
1809
+ * generated contract; rebuild is not in it). It reuses the maintenance graph (#124) to
1810
+ * resolve which maintainers exist and the shared re-derivation rules so a rebuilt value is
1811
+ * byte-consistent with a drained one.
1812
+ */
1813
+
1814
+ /** A model class (or `ModelStatic`) the rebuild planner operates over. */
1815
+ type ModelLike = Function | ModelStatic<DDBModel>;
1816
+ /** Options for {@link createMaintenanceRebuilder}. */
1817
+ interface MaintenanceRebuildOptions {
1818
+ /**
1819
+ * The model classes whose maintainers this rebuilder can repair. The rebuilder
1820
+ * builds a scoped {@link MaintenanceGraph} over them to resolve each
1821
+ * owner-row × maintainer back to its declared effect. Defaults to the GLOBAL
1822
+ * registry when omitted.
1823
+ */
1824
+ readonly models?: readonly ModelLike[];
1825
+ /**
1826
+ * The {@link ViewDefinition}s (`defineView`) whose maintainers this rebuilder can
1827
+ * repair (issue #132). The view model classes are added to the scoped registry so a
1828
+ * view row can be re-derived from its source rows, exactly like a relation owner row.
1829
+ */
1830
+ readonly views?: readonly ViewDefinition[];
1831
+ }
1832
+ /**
1833
+ * The drift verdict for one owner row × maintainer: whether the currently-stored
1834
+ * materialized value differs from the value re-derived from the source rows, and both
1835
+ * values (for diagnostics / a repair preview). `expected` is the re-derived authority;
1836
+ * `actual` is what the owner row currently holds (`undefined` when never built).
1837
+ */
1838
+ interface DriftReport {
1839
+ /** The relation / aggregate property the maintained value lives under. */
1840
+ readonly relationProperty: string;
1841
+ /** The owner entity (logical name) the row belongs to. */
1842
+ readonly ownerEntity: string;
1843
+ /** The owner row's resolved key (`{ PK, SK }`). */
1844
+ readonly ownerKey: Record<string, unknown>;
1845
+ /** The maintained attribute (collection field / snapshot attrs / counter attribute). */
1846
+ readonly kind: MaintainEffect['kind'];
1847
+ /** True when `actual` differs from `expected` (drift present). */
1848
+ readonly drifted: boolean;
1849
+ /** The value re-derived from the source rows (the authority). */
1850
+ readonly expected: unknown;
1851
+ /** The value currently stored on the owner row (`undefined` when unbuilt). */
1852
+ readonly actual: unknown;
1853
+ }
1854
+ /** The outcome of a {@link MaintenanceRebuilder.rebuild} over one owner row × maintainer. */
1855
+ interface RebuildResult extends DriftReport {
1856
+ /** True when the owner row was written (drift was present and repaired). */
1857
+ readonly repaired: boolean;
1858
+ }
1859
+ /**
1860
+ * A maintenance **repair / rebuild planner** (#131). Construct via
1861
+ * {@link createMaintenanceRebuilder}, then call {@link detectDrift} to compare or
1862
+ * {@link rebuild} to repair one owner row × maintainer (resolved by the owner model +
1863
+ * the relation/aggregate property + the owner key inputs).
1864
+ */
1865
+ declare class MaintenanceRebuilder {
1866
+ private readonly graph;
1867
+ constructor(opts?: MaintenanceRebuildOptions);
1868
+ /**
1869
+ * Re-derive the materialized value for one owner row × maintainer from the source
1870
+ * rows and report whether it differs from what the owner row currently stores. Does
1871
+ * NOT write anything (a read-only drift probe).
1872
+ *
1873
+ * @param owner The owner model class declaring the maintainer.
1874
+ * @param relationProperty The relation / `@aggregate` property the value lives under.
1875
+ * @param ownerKeyInput The owner row's key input (e.g. `{ threadId: 't1' }`).
1876
+ */
1877
+ detectDrift(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>): Promise<DriftReport>;
1878
+ /**
1879
+ * Re-derive the materialized value for one owner row × maintainer from the source
1880
+ * rows and WRITE the absolute re-derived value onto the owner row when it drifts (or
1881
+ * always, when `force`). Returns the drift verdict plus whether a repair write ran.
1882
+ *
1883
+ * @param options.force Write even when no drift is detected (default: write only on drift).
1884
+ */
1885
+ rebuild(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>, options?: {
1886
+ force?: boolean;
1887
+ }): Promise<RebuildResult>;
1888
+ /** Resolve an owner model + property to its {@link MaintainItem} + owner-key builder. */
1889
+ private resolve;
1890
+ /**
1891
+ * Scan every source row bound to this owner row and re-derive the maintained value:
1892
+ *
1893
+ * - **collection** — project each source row, order + trim by the SAME read-side
1894
+ * rules (`orderBy` / `orderDir` / `maxItems`) the drain uses, return the list;
1895
+ * - **snapshot** — project the single bound source row (or `undefined` when none);
1896
+ * - **counter `count`** — the COUNT of source rows (absolute, not an ADD);
1897
+ * - **counter `max`** — the maximum source value of the tracked field.
1898
+ */
1899
+ private recompute;
1900
+ /**
1901
+ * The source rows bound to one owner row. The maintenance key binding is
1902
+ * `{ ownerField: '$.entity.<sourceField>' }` (the destination owner-key field ← the
1903
+ * source row's field). INVERTED for a rebuild: the owner row supplies the value, and
1904
+ * the source partition is queried for rows whose `<sourceField>` matches — exactly the
1905
+ * set the live `hasMany` / `@aggregate` read aggregates. Queries the source via the
1906
+ * same {@link plan} the read planner uses (a partition Query), paginating fully.
1907
+ */
1908
+ private scanSourceRows;
1909
+ /** Re-derive a bounded collection: project, then order + trim by the read-side rules. */
1910
+ private recomputeCollection;
1911
+ /**
1912
+ * Re-derive a single-row snapshot. A snapshot mirrors ONE source row (a `belongsTo` /
1913
+ * `hasOne`, normally 1:1). In the unusual case where MORE THAN ONE source row binds, the
1914
+ * live single-value read takes the FIRST row in the source's natural query order
1915
+ * (`ScanIndexForward`, default ASC = the smallest sort-key leaf), so re-derive the same
1916
+ * row — {@link pickFirst} — for a verdict consistent with the live read. Returns
1917
+ * `undefined` when no source row is bound (the snapshot should be absent).
1918
+ */
1919
+ private recomputeSnapshot;
1920
+ /** The source entity metadata a maintain effect projects from (its relation target). */
1921
+ private sourceMetaOf;
1922
+ /** Re-derive a counter: the absolute COUNT of source rows, or the MAX of the tracked field. */
1923
+ private recomputeCounter;
1924
+ /** Read the owner row (consistent) for the drift comparison / actual value. */
1925
+ private readOwnerRow;
1926
+ /** The value currently stored on the owner row for this maintainer. */
1927
+ private actualValue;
1928
+ /** Write the re-derived ABSOLUTE value onto the owner row (the repair). */
1929
+ private writeValue;
1930
+ }
1931
+ /** Create a {@link MaintenanceRebuilder} (issue #131). */
1932
+ declare function createMaintenanceRebuilder(opts?: MaintenanceRebuildOptions): MaintenanceRebuilder;
1933
+
1422
1934
  /**
1423
1935
  * Type-level machinery for the parameterized definition DSL (issue #41).
1424
1936
  *
@@ -2290,4 +2802,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
2290
2802
  */
2291
2803
  declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
2292
2804
 
2293
- export { type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, CtxModel, DDBModel, DefinitionMap, DeleteInput, DynamoDBOperation, DynamoExecutor, type DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, type EmbeddedMetadata, type EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, type FieldMetadata, type FieldOptions, type FilterExpressionResult, type GsiDefinition, Item, type KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAX_TRANSACT_ITEMS, Manifest, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, ReadExecOptions, ReadParams, ReadRequestCtx, ReadRequestKind, type RelationLimitOptions, type RelationMetadata, type RelationOptions, type RelationTraversalOptions, ReplayOptions, RequestContext, ResolvedKey, RetryOverride, RetryPolicy, RetryingExecutor, SegmentedKey, SelectableOf, ShardId, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, createCdcEmulator, createDefaultLinter, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, map, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when };
2805
+ export { AggregateOptions, AggregateValue, type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchExecOptions, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, CtxModel, DDBModel, DefinitionMap, DeleteInput, type DriftReport, DynamoDBOperation, DynamoExecutor, DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, FieldMetadata, FieldOptions, type FilterExpressionResult, GsiDefinition, InMemoryProjectionSink, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainEffect, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, ProjectionDefinition, type ProjectionResult, type ProjectionSink, ProjectionSinkDrain, type ProjectionSinkDrainOptions, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, ReadExecOptions, ReadParams, ReadRequestCtx, ReadRequestKind, type RebuildResult, RelationMetadata, RelationOptions, type RelationTraversalOptions, ReplayOptions, RequestContext, ResolvedKey, RetryOverride, RetryPolicy, RetryingExecutor, SelectableOf, ShardId, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, ViewDefinition, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, aggregate, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, count, createCdcEmulator, createDefaultLinter, createMaintenanceDrain, createMaintenanceDrainHandler, createMaintenanceRebuilder, createProjectionSinkDrain, createProjectionSinkHandler, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when };