graphddb 0.3.0 → 0.4.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.
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 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 RelationMetadata, _ as TransactionItemSpec, $ as Param, a0 as ParamDescriptor, a1 as DefinitionMap, a2 as OperationDefinition, f as DDBModel, M as ModelStatic, a3 as WriteDefinitionOptions, a4 as PartialQueryKeyOf, a5 as StrictSelectSpec, a6 as EntityInput, a7 as UniqueQueryKeyOf, a8 as EntityRef, a9 as ConditionInput, aa as QueryModelContract, ab as QueryMethodSpec, ac as CommandModelContract, ad as CommandMethodSpec, ae as ContractSpec, af as QuerySpec, ag as CommandSpec, ah as ContextSpec, ai as OperationsDocument, aj as AnyOperationDefinition, ak as BridgeBundle, al as ConditionSpec } from './types-xJRn5qkv.js';
2
- export { am as AggregateMetadata, an as BatchDeleteRequest, ao as BatchGetOptions, ap as BatchGetRequest, aq as BatchGetResult, ar as BatchPutRequest, as as BatchResult, at as BatchWriteRequest, au as CONTRACT_RANGE_FANOUT_CONCURRENCY, av as CdcMode, aw as Change, ax as ChangeBatch, ay as ChangeEventName, az as ClockMode, aA as CollectionEffect, aB as CollectionOptions, aC as Column, aD as ColumnMap, aE as CommandContractMethodSpec, aF as CommandInputShape, aG as CommandMethod, aH as CommandPlan, aI as CommandResolutionTarget, aJ as CommandResultKind, aK as CommandSelectShape, aL as CompiledFragment, aM as ComposeSpec, aN as CondSlot, aO as ConditionCheckInput, aP as Connection, aQ as ContractCallSignature, aR as ContractCardinality, aS as ContractCommandParams, aT as ContractCommandResult, aU as ContractComposeNode, aV as ContractFromRef, aW as ContractInputArity, aX as ContractItem, aY as ContractKeyFieldRef, aZ as ContractKeyInput, a_ as ContractKeyRef, a$ as ContractKeySpec, b0 as ContractKind, b1 as ContractMethodOp, b2 as ContractParamRef, b3 as ContractQueryParams, b4 as ContractResolution, b5 as CounterAggregate, b6 as CounterEffect, b7 as CtxBase, b8 as DEFAULT_MAX_ATTEMPTS, b9 as DEFAULT_RETRY_POLICY, ba as DeleteOptions, bb as DeriveEffect, bc as DerivedEdgeWrite, bd as DerivedUpdate, be as DescriptorBinding, bf as ENTITY_WRITES_MARKER, bg as EdgeEffect, bh as EffectPath, bi as EmbeddedMetadata, bj as EmitEffect, bk as EntityWritesDefinition, bl as EntityWritesShape, bm as ExecutableCommandContract, bn as ExecutableQueryContract, bo as FilterInput, bp as FilterSpec, bq as FragmentInput, br as GsiDefinitionMarker, bs as GsiOptions, bt as IdempotencyEffect, bu as InProcessWriteDescriptor, bv as InputArity, bw as KeyDefinitionMarker, bx as KeySegment, by as KeySlot, bz as KeyStructure, bA as KeyedResult, bB as LIFECYCLE_CONTRACT_MARKER, bC as LifecycleContract, bD as LifecycleEffects, bE as LiteralParam, bF as MaintainConsistency, bG as MaintainEffect, bH as MaintainEvent, bI as MaintainItem, bJ as MaintainTrigger, bK as MaintainUpdateMode, bL as MaintenanceGraph, bM as ManifestEntity, bN as ManifestField, bO as ManifestFieldType, bP as ManifestGsi, bQ as ManifestKey, bR as ManifestRelation, bS as ManifestTable, bT as ModelRef, bU as MutateMode, bV as MutateOptions, bW as MutateParallelResult, bX as MutateTransactionResult, bY as MutationBody, bZ as MutationDescriptorMap, b_ as MutationFragment, b$ as MutationInputProxy, c0 as MutationInputRef, c1 as MutationIntent, c2 as NumberParam, c3 as OperationKind, c4 as OperationSpec, c5 as ParallelOpResult, c6 as ParamKind, c7 as ParamSpec, c8 as ParamStructure, c9 as PersistCtx, ca as PersistOrigin, cb as PlannedCommandMethod, cc as ProjectionMap, cd as ProjectionTransform, ce as ProjectionTransformOp, cf as PutOptions, cg as QueryContractMethodSpec, ch as QueryEnvelopeResult, ci as QueryKeyOf, cj as QueryMethod, ck as QueryResult, cl as RangeConditionSpec, cm as ReadEnvelope, cn as ReadOpCtx, co as ReadOpKind, cp as ReadOperationType, cq as ReadRouteDescriptor, cr as ReadRouteOptions, cs as ReadRouteResult, ct as RecordedCompose, cu as RelationBuilder, cv as RelationConsistency, cw as RelationLimitOptions, cx as RelationPattern, cy as RelationProjection, cz as RelationReadOptions, cA as RelationSelect, cB as RelationSpec, cC as RelationUpdateMode, cD as RelationWriteOptions, cE as RequiresEffect, cF as Resolution, cG as RetryInfo, cH as RetryOperationKind, cI as SPEC_VERSION, cJ as SegmentSpec, cK as SegmentedKey, cL as SelectBuilder, cM as SelectOf, cN as SnapshotEffect, cO as StartingPosition, cP as StreamViewType, cQ as StringParam, cR as TransactionContext, cS as TransactionItemType, cT as UniqueEffect, cU as Updatable, cV as UpdateOptions, cW as WhenSpec, cX as WriteCtx, cY as WriteDescriptor, cZ as WriteEnvelope, c_ as WriteInput, c$ as WriteKind, d0 as WriteLifecyclePhase, d1 as WriteMiddleware, d2 as WriteOperationType, d3 as WriteRecorder, d4 as WriteResultProjection, d5 as attachModelClass, d6 as buildDeleteInput, d7 as buildMaintenanceGraph, d8 as buildPutInput, d9 as buildUpdateInput, da as compileFragment, db as compileMutationPlan, dc as compileSingleFragmentPlan, dd as cond, de as contractOfMethodSpec, df as definePlan, dg as entityWrites, dh as executeBatchGet, di as executeBatchWrite, dj as executeCommandMethod, dk as executeDelete, dl as executeKeyedBatchGet, dm as executePut, dn as executeQueryMethod, dp as executeRangeFanout, dq as executeTransaction, dr as executeUpdate, ds as from, dt as getEntityWrites, du as gsi, dv as identity, dw as isColumn, dx as isCommandModelContract, dy as isCommandPlan, dz as isContractComposeNode, dA as isContractFromRef, dB as isContractKeyFieldRef, dC as isContractKeyRef, dD as isContractParamRef, dE as isEntityWritesDefinition, dF as isKeySegment, dG as isLifecycleContract, dH as isMaintainTrigger, dI as isMutationFragment, dJ as isMutationInputRef, dK as isParam, dL as isPlannedCommandMethod, dM as isQueryModelContract, dN as isRetryableError, dO as isRetryableTransactionCancellation, dP as k, dQ as key, dR as lifecyclePhaseForIntent, dS as maintainTrigger, dT as mintContractKeyFieldRef, dU as mintContractParamRef, dV as mutation, dW as param, dX as preview, dY as publicCommandModel, dZ as publicQueryModel, d_ as query, d$ as resolveLifecycle, e0 as wholeKeysSentinel } from './types-xJRn5qkv.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, q as ModelKind, F as FieldOptions, r as DynamoType, s as ProjectionTransform, t as MaintainEvent, u as MembershipPredicate, v as MaintainConsistency, w as MaintainUpdateMode, x as MembershipPredicateOp, y as RelationOptions, A as AggregateOptions, z as AggregateValue, H as SelectBuilderSpec, J as RawCondition, L as TransactionSpec, N as Manifest, O as RetryOverride, Q as ExecutionPlan, V as FieldMetadata, X 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, Y as CdcEmulatorOptions, Z as ChangeHandler, _ as Unsubscribe, $ as FaultSpec, a0 as ConcurrentRecomputeRef, C as ChangeEvent, a1 as EventLog, a2 as ReplayOptions, a3 as ShardId, a4 as ViewDefinition, a5 as RelationMetadata, a6 as TransactionItemSpec, a7 as MaintainEffect, M as ModelStatic, f as DDBModel, a8 as Param, a9 as ParamDescriptor, aa as DefinitionMap, ab as OperationDefinition, ac as WriteDefinitionOptions, ad as PartialQueryKeyOf, ae as StrictSelectSpec, af as EntityInput, ag as UniqueQueryKeyOf, ah as EntityRef, ai as ConditionInput, aj as QueryModelContract, ak as QueryMethodSpec, al as CommandModelContract, am as CommandMethodSpec, an as ContractSpec, ao as QuerySpec, ap as CommandSpec, aq as ContextSpec, ar as OperationsDocument, as as AnyOperationDefinition, at as BridgeBundle, au as ConditionSpec } from './types-CpCo8yHP.js';
2
+ export { av as AggregateMetadata, aw as BatchDeleteRequest, ax as BatchGetOptions, ay as BatchGetRequest, az as BatchGetResult, aA as BatchPutRequest, aB as BatchResult, aC as BatchWriteRequest, aD as CONTRACT_RANGE_FANOUT_CONCURRENCY, aE as CdcMode, aF as Change, aG as ChangeBatch, aH as ChangeEventName, aI as ClockMode, aJ as CollectionEffect, aK as CollectionOptions, aL as Column, aM as ColumnMap, aN as CommandContractMethodSpec, aO as CommandInputShape, aP as CommandMethod, aQ as CommandPlan, aR as CommandResolutionTarget, aS as CommandResultKind, aT as CommandSelectShape, aU as CompiledFragment, aV as ComposeSpec, aW as CondSlot, aX as ConditionCheckInput, aY as Connection, aZ as ContractCallSignature, a_ as ContractCardinality, a$ as ContractCommandParams, b0 as ContractCommandResult, b1 as ContractComposeNode, b2 as ContractFromRef, b3 as ContractInputArity, b4 as ContractItem, b5 as ContractKeyFieldRef, b6 as ContractKeyInput, b7 as ContractKeyRef, b8 as ContractKeySpec, b9 as ContractKind, ba as ContractMethodOp, bb as ContractParamRef, bc as ContractQueryParams, bd as ContractResolution, be as CounterAggregate, bf as CounterEffect, bg as CtxBase, bh as DEFAULT_MAX_ATTEMPTS, bi as DEFAULT_RETRY_POLICY, bj as DeleteOptions, bk as DeriveEffect, bl as DerivedEdgeWrite, bm as DerivedUpdate, bn as DescriptorBinding, bo as ENTITY_WRITES_MARKER, bp as EdgeEffect, bq as EffectPath, br as EmbeddedMetadata, bs as EmitEffect, bt as EntityWritesDefinition, bu as EntityWritesShape, bv as ExecutableCommandContract, bw as ExecutableQueryContract, bx as FilterInput, by as FilterSpec, bz as FragmentInput, bA as GsiDefinitionMarker, bB as GsiOptions, bC as IdempotencyEffect, bD as InProcessWriteDescriptor, bE as InputArity, bF as KeyDefinitionMarker, bG as KeySegment, bH as KeySlot, bI as KeyStructure, bJ as KeyedResult, bK as LIFECYCLE_CONTRACT_MARKER, bL as LifecycleContract, bM as LifecycleEffects, bN as LiteralParam, bO as MaintainItem, bP as MaintainTrigger, bQ as MaintenanceGraph, bR as ManifestEntity, bS as ManifestField, bT as ManifestFieldType, bU as ManifestGsi, bV as ManifestKey, bW as ManifestRelation, bX as ManifestTable, bY as MembershipEffect, bZ as ModelRef, b_ as MutateMode, b$ as MutateOptions, c0 as MutateParallelResult, c1 as MutateTransactionResult, c2 as MutationBody, c3 as MutationDescriptorMap, c4 as MutationFragment, c5 as MutationInputProxy, c6 as MutationInputRef, c7 as MutationIntent, c8 as NumberParam, c9 as OperationKind, ca as OperationSpec, cb as ParallelOpResult, cc as ParamKind, cd as ParamSpec, ce as ParamStructure, cf as PersistCtx, cg as PersistOrigin, ch as PlannedCommandMethod, ci as ProjectionMap, cj as ProjectionTransformOp, ck as PutOptions, cl as QueryContractMethodSpec, cm as QueryEnvelopeResult, cn as QueryKeyOf, co as QueryMethod, cp as QueryResult, cq as RangeConditionSpec, cr as ReadEnvelope, cs as ReadOpCtx, ct as ReadOpKind, cu as ReadOperationType, cv as ReadRouteDescriptor, cw as ReadRouteOptions, cx as ReadRouteResult, cy as RecordedCompose, cz as RelationBuilder, cA as RelationConsistency, cB as RelationLimitOptions, cC as RelationPattern, cD as RelationProjection, cE as RelationReadOptions, cF as RelationSelect, cG as RelationSpec, cH as RelationUpdateMode, cI as RelationWriteOptions, cJ as RequiresEffect, cK as Resolution, cL as RetryInfo, cM as RetryOperationKind, cN as SPEC_VERSION, cO as SegmentSpec, cP as SegmentedKey, cQ as SelectBuilder, cR as SelectOf, cS as SnapshotEffect, cT as StartingPosition, cU as StreamViewType, cV as StringParam, cW as TransactionContext, cX as TransactionItemType, cY as UniqueEffect, cZ as Updatable, c_ as UpdateOptions, c$ as ViewSourceSlice, d0 as WhenSpec, d1 as WriteCtx, d2 as WriteDescriptor, d3 as WriteEnvelope, d4 as WriteInput, d5 as WriteKind, d6 as WriteLifecyclePhase, d7 as WriteMiddleware, d8 as WriteOperationType, d9 as WriteRecorder, da as WriteResultProjection, db as attachModelClass, dc as buildDeleteInput, dd as buildMaintenanceGraph, de as buildPutInput, df as buildUpdateInput, dg as collectViewDefinitions, dh as compileFragment, di as compileMutationPlan, dj as compileSingleFragmentPlan, dk as cond, dl as contractOfMethodSpec, dm as definePlan, dn as entityWrites, dp as executeBatchGet, dq as executeBatchWrite, dr as executeCommandMethod, ds as executeDelete, dt as executeKeyedBatchGet, du as executePut, dv as executeQueryMethod, dw as executeRangeFanout, dx as executeTransaction, dy as executeUpdate, dz as from, dA as getEntityWrites, dB as gsi, dC as identity, dD as isColumn, dE as isCommandModelContract, dF as isCommandPlan, dG as isContractComposeNode, dH as isContractFromRef, dI as isContractKeyFieldRef, dJ as isContractKeyRef, dK as isContractParamRef, dL as isEntityWritesDefinition, dM as isKeySegment, dN as isLifecycleContract, dO as isMaintainTrigger, dP as isMutationFragment, dQ as isMutationInputRef, dR as isParam, dS as isPlannedCommandMethod, dT as isQueryModelContract, dU as isRetryableError, dV as isRetryableTransactionCancellation, dW as k, dX as key, dY as lifecyclePhaseForIntent, dZ as maintainTrigger, d_ as mintContractKeyFieldRef, d$ as mintContractParamRef, e0 as mutation, e1 as param, e2 as preview, e3 as publicCommandModel, e4 as publicQueryModel, e5 as query, e6 as resolveLifecycle, e7 as wholeKeysSentinel } from './types-CpCo8yHP.js';
3
3
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4
4
  import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
5
5
 
@@ -378,6 +378,14 @@ declare function validateGsiAmbiguity(primaryKey: KeyDefinition | null, gsiDefin
378
378
  interface ModelOptions {
379
379
  table: string;
380
380
  prefix?: string;
381
+ /**
382
+ * The model's maintenance kind (issue #152). Defaults to `'entity'` (a plain
383
+ * stored entity). `'materializedView'` / `'sparseView'` mark it a maintained view
384
+ * row whose `@maintainedFrom` declarations feed the maintenance graph. A
385
+ * `sparseView` requires every `@maintainedFrom` to carry a `when` predicate (the
386
+ * adapter validates this).
387
+ */
388
+ kind?: ModelKind;
381
389
  }
382
390
  declare function model(options: ModelOptions): (target: Function, context: ClassDecoratorContext) => void;
383
391
 
@@ -481,9 +489,294 @@ declare const map: FieldDecorator<Record<string, unknown>>;
481
489
 
482
490
  declare function embedded(modelFactory: () => new (...args: unknown[]) => unknown): (_value: undefined, context: ClassFieldDecoratorContext) => void;
483
491
 
484
- declare function hasMany(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
492
+ /**
493
+ * Symbolic `(self, source) =>` evaluation for the `@maintainedFrom` view authoring
494
+ * and the versioned relation `pattern` union (issue #152).
495
+ *
496
+ * ## Why this module exists (existing-mechanism reuse, not a new invention)
497
+ *
498
+ * `key()` / `gsi()` already evaluate an author callback by handing it a **Proxy
499
+ * accessor** (`createColumnMap`, `src/select/column.ts`): any `c.<field>` access
500
+ * returns a branded {@link import('../select/column.js').Column} sentinel carrying
501
+ * the field NAME, never a real value — so the structure (which field is
502
+ * referenced, where) is captured verbatim and a value transform is structurally
503
+ * impossible. The maintained-authoring `(self, source) =>` callback is the SAME
504
+ * shape of problem (capture *which* source/self field fills *which* output slot,
505
+ * declaratively), so it reuses the SAME Proxy-sentinel mechanism, specialized to
506
+ * the two roots the maintained vocabulary needs:
507
+ *
508
+ * - **`source.<field>`** → a {@link SourceRef} sentinel whose `path` is the
509
+ * payload-rooted `$.entity.<field>` the existing maintenance IR
510
+ * ({@link import('../define/entity-writes.js').ProjectionTransform}) reads. This
511
+ * is the value position (a projected value / key value / order-by / predicate
512
+ * operand). Typed `keyof Source`.
513
+ * - **`self.<field>`** → a {@link SelfRef} sentinel naming a field of the view /
514
+ * relation-target model (the `collection.field` position — which view attribute
515
+ * holds the maintained list). Typed `keyof Self`.
516
+ *
517
+ * The output-attribute *names* (the keys of `keyBind` / `project`) are plain
518
+ * `keyof Self` string literals — NOT computed keys — exactly the "key = name
519
+ * (typed string), value = reference (proxy)" rule the issue fixes.
520
+ *
521
+ * The lowering target is UNCHANGED: a {@link SourceRef} lowers to the same
522
+ * `$.entity.<field>` path the old string-based builder produced, so the maintenance
523
+ * IR (`ViewSourceSlice` / `MaintainEffect`) is byte-identical — only the authoring
524
+ * surface (this symbolic capture) is new.
525
+ */
526
+
527
+ /** Brand identifying a {@link SourceRef} (`source.<field>`) at runtime. */
528
+ declare const SOURCE_REF: unique symbol;
529
+ /** Brand identifying a {@link SelfRef} (`self.<field>`) at runtime. */
530
+ declare const SELF_REF: unique symbol;
531
+ /**
532
+ * A captured reference to a **source** field (`source.<field>`), the value
533
+ * position of the maintained vocabulary. Carries the field name and the
534
+ * payload-rooted `$.entity.<field>` {@link path} the maintenance IR reads. Branded
535
+ * so the adapter can distinguish a faithful reference from a literal and reject a
536
+ * coerced / mis-used reference loudly.
537
+ */
538
+ interface SourceRef {
539
+ readonly [SOURCE_REF]: true;
540
+ /** The source field name (e.g. `authorId`). */
541
+ readonly field: string;
542
+ /** The payload-rooted source value path (`$.entity.<field>`). */
543
+ readonly path: string;
544
+ }
545
+ /**
546
+ * A captured reference to a **self** field (`self.<field>`), naming a field of the
547
+ * model being defined (the view / relation-target). Used in the `collection.field`
548
+ * position (which attribute holds the maintained list). Branded.
549
+ */
550
+ interface SelfRef {
551
+ readonly [SELF_REF]: true;
552
+ /** The self (view / relation-target) field name (e.g. `latestPosts`). */
553
+ readonly field: string;
554
+ }
555
+ /**
556
+ * The typed `source` accessor handed to a `(self, source) =>` callback: one
557
+ * {@link SourceRef} per field of the source entity, each typed with the field's
558
+ * declared value type (so `preview(source.body, n)` type-checks against a string
559
+ * field and a key-value position type-checks against the bound view-key field).
560
+ *
561
+ * @typeParam S - The source entity instance type.
562
+ */
563
+ type SourceProxy<S> = [unknown] extends [S] ? Readonly<Record<string, SourceRef>> : {
564
+ readonly [K in keyof S]-?: SourceRef & {
565
+ readonly __value?: S[K];
566
+ };
567
+ };
568
+ /**
569
+ * The typed `self` accessor handed to a `(self, source) =>` callback: one
570
+ * {@link SelfRef} per field of the model being defined. Used in the value position
571
+ * of `collection.field` (which self attribute holds the maintained collection).
572
+ *
573
+ * @typeParam T - The model (view / relation-target) instance type.
574
+ */
575
+ type SelfProxy<T> = [unknown] extends [T] ? Readonly<Record<string, SelfRef>> : {
576
+ readonly [K in keyof T]-?: SelfRef & {
577
+ readonly __value?: T[K];
578
+ };
579
+ };
580
+ /**
581
+ * A projected value in a maintained `project` map: either a {@link SourceRef}
582
+ * (identity capture of a source field) or an explicit {@link ProjectionTransform}
583
+ * (e.g. `preview(source.body, 120)`). This is the value side of the "key = name,
584
+ * value = reference" rule; the key side is a `keyof Self` string literal.
585
+ */
586
+ type ProjectionValue = SourceRef | ProjectionTransform;
587
+
588
+ /**
589
+ * `@maintainedFrom` — the class-level, decorator-declarative view authoring of
590
+ * issue #152 (the Contract-layer replacement for `defineView` / `ViewRegistry`).
591
+ *
592
+ * ## What it is
593
+ *
594
+ * A maintained **view** (`@model({ kind: 'materializedView' | 'sparseView' })`) is
595
+ * its own modelled row maintained from the lifecycle events of one or more source
596
+ * entities. Each source contributes via a CLASS-LEVEL `@maintainedFrom` decorator
597
+ * (stackable — multiple sources each fill different view fields). This is the
598
+ * `@model`/`@hasMany` collector流儀: each `@maintainedFrom` pushes a
599
+ * {@link MaintainedFromMetadata} into a module-level collector and `@model` drains
600
+ * it into `EntityMetadata.maintainedFrom`.
601
+ *
602
+ * ## Symbolic `(self, source) =>` evaluation (existing-mechanism reuse)
603
+ *
604
+ * The options are authored as a `(self, source) => ({...})` callback evaluated
605
+ * eagerly at decoration time with the SAME Proxy-sentinel mechanism `key()`/`gsi()`
606
+ * use (`src/decorators/maintained-symbolic.ts`):
607
+ *
608
+ * - `source.<field>` → a {@link SourceRef} (value position) lowering to
609
+ * `$.entity.<field>` — the exact path the old string builder produced, so the IR
610
+ * is byte-identical.
611
+ * - `self.<field>` → a {@link SelfRef} naming a view field (the `collection.field`
612
+ * position).
613
+ * - Object KEYS (`keyBind` / `project` left-hand side) stay `keyof Self` string
614
+ * literals (NOT computed keys).
615
+ * - `on: ['created' | 'updated' | 'removed'][]` — the source is the decorator's
616
+ * first argument, so the `'Post.created'` logical-name string is gone (the bug
617
+ * class #124/#130 hit is removed by construction).
618
+ *
619
+ * ## Order independence (issue #152 hardening)
620
+ *
621
+ * Stacking order carries NO meaning. The adapter
622
+ * (`src/relation/maintenance-view-adapter.ts`) rejects — at build time, loudly and
623
+ * symmetrically (same error whichever order the decorators are stacked) — any two
624
+ * `@maintainedFrom` declarations that write the SAME projection target, the SAME
625
+ * `collection.field`, or bind the SAME view-key field inconsistently. A legitimate
626
+ * multi-source view (different sources filling DIFFERENT fields) is fine.
627
+ */
628
+
629
+ type AnyModelClass$1 = new (...args: any[]) => any;
630
+ type SourceFactory = () => AnyModelClass$1;
631
+ /**
632
+ * A map keyed by `keyof Self` (the typed view fields) with value type `V`. When
633
+ * `Self` is left at its `unknown` default (the un-annotated decorator), it widens to
634
+ * a permissive `Record<string, V>` so authoring works without an explicit generic;
635
+ * supply `Self` to get `keyof Self` key-name checking.
636
+ */
637
+ type SelfKeyed<Self, V> = [unknown] extends [Self] ? Readonly<Record<string, V>> : {
638
+ readonly [K in keyof Self]?: V;
639
+ };
640
+ /**
641
+ * The author-facing options object a `@maintainedFrom` callback returns. Keys
642
+ * (`keyBind` / `project` left-hand side) are `keyof Self` string literals; values
643
+ * are symbolic {@link SourceRef} (`source.<field>`) / transforms / {@link SelfRef}
644
+ * (`self.<field>`). `collection` and `when` are mutually exclusive.
645
+ *
646
+ * @typeParam Self - The view model instance type (drives `keyof Self` keys).
647
+ */
648
+ interface MaintainedFromOptions<Self = unknown> {
649
+ /** The lifecycle events of the source that maintain the view row. */
650
+ readonly on: readonly MaintainEvent[];
651
+ /**
652
+ * The view-row key binding: view-row key field (`keyof Self`) → a `source.<field>`
653
+ * reference resolving which view row this source event maintains.
654
+ */
655
+ readonly keyBind: SelfKeyed<Self, SourceRef>;
656
+ /**
657
+ * The projection of the source payload into the view row: target attribute
658
+ * (`keyof Self`) → a `source.<field>` reference or a transform (`preview(...)`).
659
+ */
660
+ readonly project: SelfKeyed<Self, ProjectionValue>;
661
+ /**
662
+ * Declares this source maintains a bounded, ordered COLLECTION slice of the view
663
+ * row. `field` is a `self.<field>` reference (which view attribute holds the
664
+ * list); `orderBy` is a `source.<field>` reference. Mutually exclusive with `when`.
665
+ */
666
+ readonly collection?: {
667
+ readonly field: SelfRef;
668
+ readonly maxItems?: number;
669
+ readonly order?: 'ASC' | 'DESC';
670
+ readonly orderBy?: SourceRef;
671
+ };
672
+ /** Sparse-view membership predicate (sparseView kind). Mutually exclusive with `collection`. */
673
+ readonly when?: MembershipPredicate;
674
+ readonly consistency?: MaintainConsistency;
675
+ readonly updateMode?: MaintainUpdateMode;
676
+ }
677
+ /** The `(self, source) =>` symbolic callback evaluated at decoration time. */
678
+ type MaintainedFromCallback<Self, Source> = (self: SelfProxy<Self>, source: SourceProxy<Source>) => MaintainedFromOptions<Self>;
679
+ /**
680
+ * Build a {@link MembershipPredicate} for a sparse view. The view row is PUT when
681
+ * the predicate holds and DELETED when it flips false. The operand is a symbolic
682
+ * `source.<field>` reference (or a bare path string) lowered to `$.entity.<field>`.
683
+ *
684
+ * Named `whenMember` (not `when`) to avoid colliding with the transaction-condition
685
+ * `when` helper.
686
+ *
687
+ * @example `whenMember(source.status, 'eq', 'active')`
688
+ * @example `whenMember(source.archived, 'falsy')`
689
+ */
690
+ declare function whenMember(operand: SourceRef | string, op: MembershipPredicateOp, value?: unknown): MembershipPredicate;
691
+ /**
692
+ * `@maintainedFrom(source, (self, source) => options)` — the class-level view
693
+ * maintainer decorator (issue #152). Stackable: apply once per source. Evaluates
694
+ * the symbolic callback at decoration time and records the lowered
695
+ * {@link MaintainedFromMetadata} into the collector for `@model` to drain.
696
+ *
697
+ * @typeParam Self - The view model instance type (typed `keyof Self` keys).
698
+ * @typeParam Source - The source entity instance type (typed `source.<field>`).
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * @model({ table: 'AppTable', kind: 'materializedView' })
703
+ * @maintainedFrom(() => PostModel, (self, source) => ({
704
+ * keyBind: { userId: source.authorId, threadId: source.threadId },
705
+ * on: ['created', 'removed'],
706
+ * project: { lastPostAt: source.createdAt, textPreview: preview(source.body, 120) },
707
+ * collection: { field: self.latestPosts, orderBy: source.createdAt, order: 'DESC', maxItems: 20 },
708
+ * }))
709
+ * class UserThreadListModel extends DDBModel { … }
710
+ * ```
711
+ */
712
+ declare function maintainedFrom<Self = unknown, Source = unknown>(source: SourceFactory, callback: MaintainedFromCallback<Self, Source>): (target: Function, context: ClassDecoratorContext) => void;
713
+
714
+ /**
715
+ * Versioned relation `pattern` discriminated union (issue #152, section B).
716
+ *
717
+ * Versioned is "a preset that re-uses the core snapshot effect", so it rides on the
718
+ * `@hasOne` / `@hasMany` `pattern` discriminator instead of a dedicated decorator:
719
+ *
720
+ * - `@hasOne(() => Latest, (self, source) => ({ pattern: 'versionedLatest', on, project }))`
721
+ * — a SINGLE maintained row, OVERWRITTEN per revision (keyed without the version).
722
+ * - `@hasMany(() => History, (self, source) => ({ pattern: 'versionedHistory', on, project }))`
723
+ * — one APPENDED row per revision (keyed WITH the version).
724
+ *
725
+ * ## Three type-level guards (issue #152 B (a)/(b)/(c))
726
+ *
727
+ * (a) Per-pattern fields — the options union is discriminated on `pattern`, so each
728
+ * member admits exactly its own fields.
729
+ * (b) Cardinality integrity — `versionedLatest` is only assignable to a `@hasOne`
730
+ * callback's return and `versionedHistory` only to `@hasMany`'s, enforced by the
731
+ * distinct callback option types {@link HasOneVersionedOptions} /
732
+ * {@link HasManyVersionedOptions} the two decorators accept.
733
+ * (c) Symbolic refs — `source.<field>` is `keyof Source` and `project` keys are
734
+ * `keyof Self`, via the shared {@link SourceProxy} / proxy-sentinel mechanism.
735
+ *
736
+ * Both lower to the SAME snapshot×2 IR the old `defineVersioned` produced (the
737
+ * adapter `src/relation/maintenance-view-adapter.ts` turns the recorded
738
+ * `RelationOptions.versioned` into `defineView`-shaped `ViewDefinition`s). The
739
+ * latest/history KEY binding is derived from the TARGET model's own key input fields
740
+ * (each bound from the same-named source field) — latest keys omit the version,
741
+ * history keys include it, by the two models' own `key` definitions.
742
+ */
743
+
744
+ /** A versioned-latest options object (single overwritten row). */
745
+ interface VersionedLatestOptions<Self = unknown> {
746
+ readonly pattern: 'versionedLatest';
747
+ readonly on: readonly MaintainEvent[];
748
+ readonly project: SelfKeyed<Self, ProjectionValue>;
749
+ readonly consistency?: MaintainConsistency;
750
+ readonly updateMode?: MaintainUpdateMode;
751
+ }
752
+ /** A versioned-history options object (append-only per-revision rows). */
753
+ interface VersionedHistoryOptions<Self = unknown> {
754
+ readonly pattern: 'versionedHistory';
755
+ readonly on: readonly MaintainEvent[];
756
+ readonly project: SelfKeyed<Self, ProjectionValue>;
757
+ readonly consistency?: MaintainConsistency;
758
+ readonly updateMode?: MaintainUpdateMode;
759
+ }
760
+ /**
761
+ * The options a `@hasOne` versioned callback may return — `versionedLatest` ONLY.
762
+ * (Cardinality integrity: a `versionedHistory` on `@hasOne` is a type error.)
763
+ */
764
+ type HasOneVersionedOptions<Self = unknown> = VersionedLatestOptions<Self>;
765
+ /**
766
+ * The options a `@hasMany` versioned callback may return — `versionedHistory` ONLY.
767
+ * (Cardinality integrity: a `versionedLatest` on `@hasMany` is a type error.)
768
+ */
769
+ type HasManyVersionedOptions<Self = unknown> = VersionedHistoryOptions<Self>;
770
+ /** The `(self, source) =>` versioned callback for `@hasOne`. */
771
+ type HasOneVersionedCallback<Self, Source> = (self: SelfProxy<Self>, source: SourceProxy<Source>) => HasOneVersionedOptions<Self>;
772
+ /** The `(self, source) =>` versioned callback for `@hasMany`. */
773
+ type HasManyVersionedCallback<Self, Source> = (self: SelfProxy<Self>, source: SourceProxy<Source>) => HasManyVersionedOptions<Self>;
774
+
775
+ declare function hasMany(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (value: undefined, context: ClassFieldDecoratorContext) => void;
776
+ declare function hasMany<Self = unknown, Source = unknown>(targetFactory: () => new (...args: unknown[]) => unknown, callback: HasManyVersionedCallback<Self, Source>): (value: undefined, context: ClassFieldDecoratorContext) => void;
485
777
  declare function belongsTo(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>, options?: RelationOptions): (_value: undefined, context: ClassFieldDecoratorContext) => void;
486
- declare function hasOne(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>): (_value: undefined, context: ClassFieldDecoratorContext) => void;
778
+ declare function hasOne(targetFactory: () => new (...args: unknown[]) => unknown, keyBinding: Record<string, string>): (value: undefined, context: ClassFieldDecoratorContext) => void;
779
+ declare function hasOne<Self = unknown, Source = unknown>(targetFactory: () => new (...args: unknown[]) => unknown, callback: HasOneVersionedCallback<Self, Source>): (value: undefined, context: ClassFieldDecoratorContext) => void;
487
780
 
488
781
  /**
489
782
  * `@aggregate` — a **scalar-aggregate** field decorator (Epic #118 §5.2 counter /
@@ -1075,6 +1368,147 @@ declare class CdcEmulator {
1075
1368
  /** Create a CDC emulator (spec §10). */
1076
1369
  declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
1077
1370
 
1371
+ /**
1372
+ * CDC **maintenance drain** (issue #130) — the asynchronous lower for
1373
+ * `updateMode: 'stream'` maintainers (Epic #118 §5.1).
1374
+ *
1375
+ * A synchronous (`updateMode: 'mutation'`) maintainer composes its owner-row write
1376
+ * into the SAME atomic `TransactWriteItems` as the source write (#127). A
1377
+ * `updateMode: 'stream'` maintainer instead emits a **maintenance-outbox** marker row
1378
+ * (`OUTBOX#MAINT#…`) ATOMICALLY with the source write (the #130 compile lowering in
1379
+ * `src/spec/mutation-command.ts`), and THIS consumer — driven by the {@link
1380
+ * import('./emulator.js').CdcEmulator} (or, in production, real DynamoDB Streams +
1381
+ * a Lambda built around {@link createMaintenanceDrainHandler}) — applies the owner-row
1382
+ * write asynchronously.
1383
+ *
1384
+ * ## Why a separate async path exists (not just "do it later synchronously")
1385
+ *
1386
+ * The async path realizes the maintenance operations a single synchronous
1387
+ * `UpdateExpression` cannot express against an unread item:
1388
+ *
1389
+ * - **collection `maxItems` trim** — keeping a bounded, ordered list requires reading
1390
+ * the current list, appending, sorting, and trimming (a read-modify-write); a
1391
+ * synchronous `list_append` can only append (Phase 1 was append-only, #127).
1392
+ * - **`removed`-driven splice** — removing an entry from a maintained collection on a
1393
+ * source `removed` event likewise needs the current list.
1394
+ * - **running `max(field)`** — a conditional `SET` (`#a = :v` guarded by
1395
+ * `attribute_not_exists(#a) OR #a < :v`) whose failed guard, if run synchronously,
1396
+ * would roll back the legitimate source write.
1397
+ *
1398
+ * ## Delivery guarantees (reused from the CDC substrate)
1399
+ *
1400
+ * The maintenance-outbox row is recorded ATOMICALLY with the source write, so the
1401
+ * intent can never be lost ("wrote the row, lost the maintenance" is impossible). The
1402
+ * CDC substrate then delivers it **at-least-once**, **per-shard ordered** (a shard is
1403
+ * `hash(pk)`, and the outbox row's `pk` is keyed off the SOURCE row's key, so all
1404
+ * maintenance events for one source entity land on one shard in source-write order),
1405
+ * with a **DLQ** for poison events. This consumer is therefore written to be
1406
+ * **idempotent / commutative** wherever at-least-once redelivery could double-apply:
1407
+ *
1408
+ * - a `snapshot` `SET` is naturally idempotent (re-applying the same projection is a
1409
+ * no-op);
1410
+ * - a `collection` rebuild de-duplicates by the projected item's identity key, so a
1411
+ * redelivered append does not grow the list twice;
1412
+ * - a `max` conditional `SET` is idempotent (the guard rejects a non-greater value);
1413
+ * - a `count` `ADD` is the ONE non-idempotent op — but the source-row create guard
1414
+ * (`attribute_not_exists`) means a given source `created` event is emitted at most
1415
+ * once into the outbox, and the per-shard checkpoint advances only over acked events,
1416
+ * so a `count` is applied once per genuine source lifecycle. (A duplicate DELIVERY of
1417
+ * the same event is folded by the consumer's per-event de-dup set within a drain.)
1418
+ */
1419
+
1420
+ /**
1421
+ * The PK prefix a maintenance-outbox row keys on — the single source of truth shared
1422
+ * with the compiler ({@link import('../spec/mutation-command.js')}'s
1423
+ * `MAINT_OUTBOX_PK_PREFIX`). The drain selects its events by `keys.pk.startsWith` of
1424
+ * this. Kept as a local literal (rather than importing a non-exported compiler const)
1425
+ * so the cdc module does not depend on the spec module; the value is asserted equal by
1426
+ * the integration tests that round-trip a real stream maintainer through both.
1427
+ */
1428
+ declare const MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
1429
+ /** Options for {@link createMaintenanceDrain}. */
1430
+ interface MaintenanceDrainOptions {
1431
+ /**
1432
+ * The model classes (or `ModelStatic`s) whose stream maintainers this drain
1433
+ * applies. The drain builds a scoped {@link MaintenanceGraph} over them to resolve
1434
+ * each maintenance-outbox event back to its declared effect. Defaults to the GLOBAL
1435
+ * registry when omitted.
1436
+ */
1437
+ readonly models?: readonly Function[];
1438
+ /**
1439
+ * The {@link ViewDefinition}s (`defineView`) whose maintainers this drain applies
1440
+ * (issue #132). A view's destination is its dedicated view model; its source
1441
+ * classes should also be in {@link models} so the drain can resolve their
1442
+ * maintenance-outbox events. Defaults to none.
1443
+ */
1444
+ readonly views?: readonly ViewDefinition[];
1445
+ }
1446
+ /**
1447
+ * A maintenance drain: a CDC consumer ({@link ChangeHandler}) that applies the
1448
+ * owner-row writes for `updateMode: 'stream'` maintainers. Construct via
1449
+ * {@link createMaintenanceDrain}, then `subscribe` its {@link handler} to a
1450
+ * {@link import('./emulator.js').CdcEmulator} (dev/test) or wire it into a real
1451
+ * Streams Lambda (production).
1452
+ */
1453
+ declare class MaintenanceDrain {
1454
+ private readonly graph;
1455
+ /** Owner entity name → its model class (for the owner-row key derivation). */
1456
+ private readonly ownerByName;
1457
+ /** Per-event-id de-dup within a single handler invocation (at-least-once). */
1458
+ private appliedSeq;
1459
+ /** Count of owner-row writes applied (test/diagnostic). */
1460
+ private applied;
1461
+ constructor(opts?: MaintenanceDrainOptions);
1462
+ /** The CDC {@link ChangeHandler} to subscribe to an emulator / Streams source. */
1463
+ readonly handler: ChangeHandler;
1464
+ /** Resolve one maintenance-outbox event to its effect and apply the owner-row write. */
1465
+ private applyOne;
1466
+ /** Resolve a maintenance-outbox event back to the declared {@link MaintainItem}. */
1467
+ private resolve;
1468
+ /** A `snapshot` SET of each projected attribute onto the owner row (idempotent). */
1469
+ private applySnapshot;
1470
+ /**
1471
+ * A sparse-view **membership** write (#133): evaluate the membership predicate against
1472
+ * the source image; PUT the view row (its projection + key fields) when the predicate
1473
+ * holds, DELETE it when it flips false. A source `removed` event always deletes (the
1474
+ * source no longer exists, so its view row must disappear regardless of the predicate).
1475
+ *
1476
+ * Both ops are idempotent under at-least-once redelivery: a repeated PUT writes the same
1477
+ * row, a repeated DELETE is a no-op. The view row is keyed by the source identity
1478
+ * (`effect.keys`), so the PUT/DELETE always targets the SAME physical row the predicate
1479
+ * gates.
1480
+ */
1481
+ private applyMembership;
1482
+ /**
1483
+ * A running `max`: read the owner row, and `SET` the attribute to the source value
1484
+ * only when it is greater (or absent). Implemented as a conditional update so a
1485
+ * redelivered / out-of-order older value never regresses the stored max — idempotent
1486
+ * and commutative under at-least-once delivery.
1487
+ */
1488
+ private applyMax;
1489
+ /**
1490
+ * A bounded `collection`: read the current list, apply the event (append for a
1491
+ * `created`/`updated` source, splice for a `removed`), de-duplicate by the projected
1492
+ * identity key, order by `orderBy` (in `orderDir`, default DESC), trim to `maxItems`, and write the whole
1493
+ * list back. The read-modify-write is exactly what a single synchronous
1494
+ * `UpdateExpression` cannot do — the reason a bounded/ordered collection is a stream
1495
+ * maintainer (Phase 1 sync was append-only).
1496
+ */
1497
+ private applyCollection;
1498
+ /** Owner-row writes applied so far (test/diagnostic). */
1499
+ appliedCount(): number;
1500
+ /** Clear the per-delivery de-dup set + counters (test reset). */
1501
+ reset(): void;
1502
+ }
1503
+ /** Create a {@link MaintenanceDrain} (issue #130). */
1504
+ declare function createMaintenanceDrain(opts?: MaintenanceDrainOptions): MaintenanceDrain;
1505
+ /**
1506
+ * Create just the CDC {@link ChangeHandler} for a maintenance drain — the form to
1507
+ * subscribe to an emulator or wire into a production Streams Lambda. A thin wrapper
1508
+ * over {@link createMaintenanceDrain} for callers that only need the handler.
1509
+ */
1510
+ declare function createMaintenanceDrainHandler(opts?: MaintenanceDrainOptions): ChangeHandler;
1511
+
1078
1512
  /**
1079
1513
  * Per-key cursor envelope for batched `range` (`list`) contract methods (issue
1080
1514
  * #62, CQRS single-service runtime; spec `docs/cqrs-contract.md`,
@@ -1340,12 +1774,42 @@ type EdgeLifecycle = 'onCreate' | 'onDelete' | 'onUpdateKeyChange';
1340
1774
  * lifecycle-agnostic — the *same* edge is created on create, deleted on delete,
1341
1775
  * and moved on a foreign-key change; {@link deriveEdgeWriteItems} fans it out
1342
1776
  * into the three lifecycle forms.
1777
+ *
1778
+ * ## `inverse` — dual-edge synchronization (Epic #118 pattern 9, issue #133)
1779
+ *
1780
+ * The plain edge above is **one physical row** that serves *both* read directions:
1781
+ * the base PK reads one direction and an inverse GSI (auto-maintained by DynamoDB)
1782
+ * reads the other, so a bidirectional relation costs a single write. The RFC §1
1783
+ * pattern-9 gap is the case where the inverse direction is NOT served by that GSI
1784
+ * but by a **second physical row** on its own partition — the genuine *dual edge*
1785
+ * (forward + inverse rows). {@link inverse}, when present, names a SECOND adjacency
1786
+ * model + relation property whose row is the inverse edge; {@link
1787
+ * deriveEdgeWriteItemsFor} then maintains BOTH rows from this single (one-direction)
1788
+ * declaration — the create `Put`s both, the delete `Delete`s both, a foreign-key
1789
+ * change `Delete`s+`Put`s both — so the two directions can never drift. Omit
1790
+ * `inverse` for the historical single-row-plus-GSI bidirectional edge.
1343
1791
  */
1344
1792
  interface EdgeWriteDeclaration {
1345
1793
  /** Resolves the target model whose relation this edge feeds (e.g. `GroupModel`). */
1346
1794
  readonly targetFactory: () => new (...args: unknown[]) => unknown;
1347
1795
  /** The relation property on the target that reads this edge (e.g. `members`). */
1348
1796
  readonly relationProperty: string;
1797
+ /**
1798
+ * The INVERSE edge to keep in sync (Epic #118 pattern 9 / issue #133, dual-edge).
1799
+ * Names the second adjacency model whose own row materializes the reverse-direction
1800
+ * relation. When present, every lifecycle derivation maintains the inverse row
1801
+ * alongside the forward row in the SAME transaction, so a one-direction declaration
1802
+ * keeps a two-row bidirectional edge consistent. Absent = the historical single-row
1803
+ * (base + inverse GSI) bidirectional edge.
1804
+ */
1805
+ readonly inverse?: {
1806
+ /** Resolves the inverse adjacency model whose row IS the inverse edge. */
1807
+ readonly adjacencyFactory: () => new (...args: unknown[]) => unknown;
1808
+ /** Resolves the target model whose relation the inverse row feeds. */
1809
+ readonly targetFactory: () => new (...args: unknown[]) => unknown;
1810
+ /** The relation property on the inverse target that reads the inverse row. */
1811
+ readonly relationProperty: string;
1812
+ };
1349
1813
  }
1350
1814
  /**
1351
1815
  * Marker carried by a model's static `writes` member ({@link edgeWrites}), so it
@@ -1386,6 +1850,34 @@ interface EdgeWriteRecorder {
1386
1850
  * `relationProperty` relation (→ adjacency `Delete`(old) + `Put`(new)).
1387
1851
  */
1388
1852
  updateKeyChangeEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeWriteDeclaration;
1853
+ /**
1854
+ * Declare a **dual edge** (Epic #118 pattern 9 / issue #133): a bidirectional edge
1855
+ * maintained as TWO physical rows — a forward row on THIS adjacency entity (read by
1856
+ * `forward.target.relationProperty`) and an inverse row on a SECOND adjacency model
1857
+ * (read by `inverse.target.relationProperty`). Both rows are written / deleted /
1858
+ * moved together from this single declaration, so the two directions stay in sync
1859
+ * without an inverse GSI. Use this when the reverse direction lives on its own
1860
+ * partition (a second row) rather than a GSI of the forward row.
1861
+ *
1862
+ * @example
1863
+ * ```ts
1864
+ * // On the forward adjacency entity (FollowEdge: FOLLOWER#a / FOLLOWING#b):
1865
+ * static readonly writes = edgeWrites((w) => [
1866
+ * w.dualEdge(
1867
+ * { target: () => UserModel, relationProperty: 'following' },
1868
+ * { adjacency: () => FollowedByEdgeModel, target: () => UserModel, relationProperty: 'followers' },
1869
+ * ),
1870
+ * ]);
1871
+ * ```
1872
+ */
1873
+ dualEdge(forward: {
1874
+ target: () => new (...args: unknown[]) => unknown;
1875
+ relationProperty: string;
1876
+ }, inverse: {
1877
+ adjacency: () => new (...args: unknown[]) => unknown;
1878
+ target: () => new (...args: unknown[]) => unknown;
1879
+ relationProperty: string;
1880
+ }): EdgeWriteDeclaration;
1389
1881
  }
1390
1882
  /**
1391
1883
  * Declare an adjacency entity's edge write side (issue #82), as the model's
@@ -1457,6 +1949,182 @@ declare function getEdgeWrites(modelClass: new (...args: unknown[]) => unknown):
1457
1949
  */
1458
1950
  declare function deriveModelEdgeWriteItems(modelClass: new (...args: unknown[]) => unknown, lifecycle: EdgeLifecycle): TransactionItemSpec[];
1459
1951
 
1952
+ /**
1953
+ * Maintenance **repair / rebuild planner** (Epic #118 Phase 2, issue #131).
1954
+ *
1955
+ * ## What this is
1956
+ *
1957
+ * The async drain ({@link import('../cdc/maintenance-drain.js')}, #130) keeps a
1958
+ * maintained owner row (a snapshot / collection / counter) in sync INCREMENTALLY —
1959
+ * it applies one source event at a time. Over a long-lived table the materialized
1960
+ * value can nevertheless **drift** from the source of truth: a dropped / dead-lettered
1961
+ * stream event, a maintainer added AFTER source rows already existed (initial
1962
+ * un-built state), a bug in an earlier maintainer version, or an out-of-band write.
1963
+ *
1964
+ * The rebuild planner is the repair for that drift. For a given owner row × maintainer
1965
+ * it **re-derives the owner-row value from scratch by scanning every source row that
1966
+ * belongs to that owner**, using the SAME projection / aggregation / ordering rules the
1967
+ * drain uses (shared via `maintenance-projection.ts` and this module), then:
1968
+ *
1969
+ * - **{@link detectDrift}** — compares the re-derived value against the value currently
1970
+ * stored on the owner row and reports whether (and how) they differ; or
1971
+ * - **{@link rebuild}** — writes the re-derived ABSOLUTE value onto the owner row, so the
1972
+ * materialized data converges to exactly what the source says.
1973
+ *
1974
+ * After a rebuild the owner row equals what an incremental drain would have converged to
1975
+ * had it never missed an event — the AC: "rebuild で materialized が source と一致する".
1976
+ *
1977
+ * ## Granularity: per owner row × maintainer
1978
+ *
1979
+ * Drift is detected and repaired at the **owner-row × maintainer** grain — the natural
1980
+ * unit, because one owner row's materialized value is a pure function of the set of
1981
+ * source rows bound to it. The caller names the owner rows to repair (their key inputs);
1982
+ * the planner discovers the source rows for each by querying the source on the maintenance
1983
+ * key binding (the same binding the live `hasMany` / `@aggregate` read uses, inverted:
1984
+ * the owner row supplies the binding value, the source partition is scanned for matches).
1985
+ *
1986
+ * ## count is re-derived as an ABSOLUTE SET, not an ADD
1987
+ *
1988
+ * The maintained counter applies a signed `ADD` per event (#141 / #130). A rebuild
1989
+ * instead **counts the source rows and SETs the absolute value** — it does not trust the
1990
+ * accumulated counter, it recomputes it. This is the whole point of repair (an ADD that
1991
+ * has drifted cannot be corrected by another ADD). The `max(field)` counter is re-derived
1992
+ * as the maximum over the scanned source rows and SET unconditionally (the rebuild is the
1993
+ * authority, so it does not use the drain's advance-only conditional guard).
1994
+ *
1995
+ * ## Execution form (production positioning)
1996
+ *
1997
+ * Like the drain applier, this is a **TS-side planner/executor** intended for dev/test and
1998
+ * **operational repair tooling** — run on-demand (a "repair this thread's summary now"
1999
+ * admin action) or as a periodic batch (a scheduled reconciliation sweep over a set of
2000
+ * owner rows). It is NOT injected into the SSoT spec and emits no generated artifact: a
2001
+ * rebuild is a host-side reconciliation against live DynamoDB, not part of a model's
2002
+ * compiled write contract — so there is no Python mirror (the Python runtime executes the
2003
+ * generated contract; rebuild is not in it). It reuses the maintenance graph (#124) to
2004
+ * resolve which maintainers exist and the shared re-derivation rules so a rebuilt value is
2005
+ * byte-consistent with a drained one.
2006
+ */
2007
+
2008
+ /** A model class (or `ModelStatic`) the rebuild planner operates over. */
2009
+ type ModelLike = Function | ModelStatic<DDBModel>;
2010
+ /** Options for {@link createMaintenanceRebuilder}. */
2011
+ interface MaintenanceRebuildOptions {
2012
+ /**
2013
+ * The model classes whose maintainers this rebuilder can repair. The rebuilder
2014
+ * builds a scoped {@link MaintenanceGraph} over them to resolve each
2015
+ * owner-row × maintainer back to its declared effect. Defaults to the GLOBAL
2016
+ * registry when omitted.
2017
+ */
2018
+ readonly models?: readonly ModelLike[];
2019
+ /**
2020
+ * The {@link ViewDefinition}s (`defineView`) whose maintainers this rebuilder can
2021
+ * repair (issue #132). The view model classes are added to the scoped registry so a
2022
+ * view row can be re-derived from its source rows, exactly like a relation owner row.
2023
+ */
2024
+ readonly views?: readonly ViewDefinition[];
2025
+ }
2026
+ /**
2027
+ * The drift verdict for one owner row × maintainer: whether the currently-stored
2028
+ * materialized value differs from the value re-derived from the source rows, and both
2029
+ * values (for diagnostics / a repair preview). `expected` is the re-derived authority;
2030
+ * `actual` is what the owner row currently holds (`undefined` when never built).
2031
+ */
2032
+ interface DriftReport {
2033
+ /** The relation / aggregate property the maintained value lives under. */
2034
+ readonly relationProperty: string;
2035
+ /** The owner entity (logical name) the row belongs to. */
2036
+ readonly ownerEntity: string;
2037
+ /** The owner row's resolved key (`{ PK, SK }`). */
2038
+ readonly ownerKey: Record<string, unknown>;
2039
+ /** The maintained attribute (collection field / snapshot attrs / counter attribute). */
2040
+ readonly kind: MaintainEffect['kind'];
2041
+ /** True when `actual` differs from `expected` (drift present). */
2042
+ readonly drifted: boolean;
2043
+ /** The value re-derived from the source rows (the authority). */
2044
+ readonly expected: unknown;
2045
+ /** The value currently stored on the owner row (`undefined` when unbuilt). */
2046
+ readonly actual: unknown;
2047
+ }
2048
+ /** The outcome of a {@link MaintenanceRebuilder.rebuild} over one owner row × maintainer. */
2049
+ interface RebuildResult extends DriftReport {
2050
+ /** True when the owner row was written (drift was present and repaired). */
2051
+ readonly repaired: boolean;
2052
+ }
2053
+ /**
2054
+ * A maintenance **repair / rebuild planner** (#131). Construct via
2055
+ * {@link createMaintenanceRebuilder}, then call {@link detectDrift} to compare or
2056
+ * {@link rebuild} to repair one owner row × maintainer (resolved by the owner model +
2057
+ * the relation/aggregate property + the owner key inputs).
2058
+ */
2059
+ declare class MaintenanceRebuilder {
2060
+ private readonly graph;
2061
+ constructor(opts?: MaintenanceRebuildOptions);
2062
+ /**
2063
+ * Re-derive the materialized value for one owner row × maintainer from the source
2064
+ * rows and report whether it differs from what the owner row currently stores. Does
2065
+ * NOT write anything (a read-only drift probe).
2066
+ *
2067
+ * @param owner The owner model class declaring the maintainer.
2068
+ * @param relationProperty The relation / `@aggregate` property the value lives under.
2069
+ * @param ownerKeyInput The owner row's key input (e.g. `{ threadId: 't1' }`).
2070
+ */
2071
+ detectDrift(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>): Promise<DriftReport>;
2072
+ /**
2073
+ * Re-derive the materialized value for one owner row × maintainer from the source
2074
+ * rows and WRITE the absolute re-derived value onto the owner row when it drifts (or
2075
+ * always, when `force`). Returns the drift verdict plus whether a repair write ran.
2076
+ *
2077
+ * @param options.force Write even when no drift is detected (default: write only on drift).
2078
+ */
2079
+ rebuild(owner: ModelLike, relationProperty: string, ownerKeyInput: Record<string, unknown>, options?: {
2080
+ force?: boolean;
2081
+ }): Promise<RebuildResult>;
2082
+ /** Resolve an owner model + property to its {@link MaintainItem} + owner-key builder. */
2083
+ private resolve;
2084
+ /**
2085
+ * Scan every source row bound to this owner row and re-derive the maintained value:
2086
+ *
2087
+ * - **collection** — project each source row, order + trim by the SAME read-side
2088
+ * rules (`orderBy` / `orderDir` / `maxItems`) the drain uses, return the list;
2089
+ * - **snapshot** — project the single bound source row (or `undefined` when none);
2090
+ * - **counter `count`** — the COUNT of source rows (absolute, not an ADD);
2091
+ * - **counter `max`** — the maximum source value of the tracked field.
2092
+ */
2093
+ private recompute;
2094
+ /**
2095
+ * The source rows bound to one owner row. The maintenance key binding is
2096
+ * `{ ownerField: '$.entity.<sourceField>' }` (the destination owner-key field ← the
2097
+ * source row's field). INVERTED for a rebuild: the owner row supplies the value, and
2098
+ * the source partition is queried for rows whose `<sourceField>` matches — exactly the
2099
+ * set the live `hasMany` / `@aggregate` read aggregates. Queries the source via the
2100
+ * same {@link plan} the read planner uses (a partition Query), paginating fully.
2101
+ */
2102
+ private scanSourceRows;
2103
+ /** Re-derive a bounded collection: project, then order + trim by the read-side rules. */
2104
+ private recomputeCollection;
2105
+ /**
2106
+ * Re-derive a single-row snapshot. A snapshot mirrors ONE source row (a `belongsTo` /
2107
+ * `hasOne`, normally 1:1). In the unusual case where MORE THAN ONE source row binds, the
2108
+ * live single-value read takes the FIRST row in the source's natural query order
2109
+ * (`ScanIndexForward`, default ASC = the smallest sort-key leaf), so re-derive the same
2110
+ * row — {@link pickFirst} — for a verdict consistent with the live read. Returns
2111
+ * `undefined` when no source row is bound (the snapshot should be absent).
2112
+ */
2113
+ private recomputeSnapshot;
2114
+ /** The source entity metadata a maintain effect projects from (its relation target). */
2115
+ private sourceMetaOf;
2116
+ /** Re-derive a counter: the absolute COUNT of source rows, or the MAX of the tracked field. */
2117
+ private recomputeCounter;
2118
+ /** Read the owner row (consistent) for the drift comparison / actual value. */
2119
+ private readOwnerRow;
2120
+ /** The value currently stored on the owner row for this maintainer. */
2121
+ private actualValue;
2122
+ /** Write the re-derived ABSOLUTE value onto the owner row (the repair). */
2123
+ private writeValue;
2124
+ }
2125
+ /** Create a {@link MaintenanceRebuilder} (issue #131). */
2126
+ declare function createMaintenanceRebuilder(opts?: MaintenanceRebuildOptions): MaintenanceRebuilder;
2127
+
1460
2128
  /**
1461
2129
  * Type-level machinery for the parameterized definition DSL (issue #41).
1462
2130
  *
@@ -2328,4 +2996,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
2328
2996
  */
2329
2997
  declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
2330
2998
 
2331
- 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, 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, Item, 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, 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, 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, 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 };
2999
+ 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, type HasManyVersionedCallback, type HasManyVersionedOptions, type HasOneVersionedCallback, type HasOneVersionedOptions, Item, KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAINT_OUTBOX_PK_PREFIX, MAX_TRANSACT_ITEMS, MaintainConsistency, MaintainEffect, MaintainEvent, MaintainUpdateMode, type MaintainedFromCallback, type MaintainedFromOptions, MaintenanceDrain, type MaintenanceDrainOptions, type MaintenanceRebuildOptions, MaintenanceRebuilder, Manifest, MembershipPredicate, MembershipPredicateOp, MetadataRegistry, Middleware, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, ProjectionTransform, type ProjectionValue, 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, type SelfProxy, type SelfRef, ShardId, type SourceProxy, type SourceRef, 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 VersionedHistoryOptions, type VersionedLatestOptions, 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, 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, maintainedFrom, map, max, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when, whenMember };