graphddb 0.1.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/README.md ADDED
@@ -0,0 +1,553 @@
1
+ # GraphDDB
2
+
3
+ **Type-safe Graph Query Runtime for DynamoDB**
4
+
5
+ GraphDDB maps DynamoDB's native access patterns to a GraphQL-like query model:
6
+ PK, GSI, Projection, Relation, Cursor, and operation limits all remain visible.
7
+
8
+ GraphDDB is not an ORM.
9
+ It is a runtime for defining, validating, and executing DynamoDB access patterns with TypeScript types.
10
+
11
+ ## Why GraphDDB?
12
+
13
+ DynamoDB is fast and scalable, but easy to design incorrectly. Teams often treat it like an
14
+ RDBMS — one table per entity, joins in application code, ad-hoc access patterns hidden behind
15
+ ORM abstractions — and end up with unstable performance, unnecessary GSIs, and unclear service
16
+ boundaries.
17
+
18
+ GraphDDB takes the opposite approach: it represents DynamoDB access patterns **directly in
19
+ code**, keeping PK/GSI, projections, relations, cursors, and operation limits visible. Access
20
+ patterns and relations become explicit, query plans become inspectable, and well-designed
21
+ access patterns naturally align with service boundaries. GraphDDB does not teach DynamoDB
22
+ design — it makes correct design the easiest way to write code.
23
+
24
+ It does this through a natural correspondence between GraphQL's query model and DynamoDB's
25
+ access patterns:
26
+
27
+ ```text
28
+ GraphQL Query Model DynamoDB
29
+ arguments ----> PK / GSI lookup
30
+ selection ----> ProjectionExpression
31
+ relation ----> Query / Get / BatchGet
32
+ connection ----> Cursor pagination
33
+ complexity ----> Operation limits
34
+ ```
35
+
36
+ A partition key is a graph entry point, a relation an explicit traversal path, a projection a
37
+ selection set. GraphDDB does not invent a query language — it executes well-designed access
38
+ patterns directly and makes ambiguity visible through plans, limits, and relation resolution.
39
+
40
+ ## User Permissions Example
41
+
42
+ The [user-permissions](./examples/user-permissions/) example models users, groups, memberships, and permissions in a single DynamoDB table.
43
+ This is where GraphDDB's design becomes easiest to see: the table is physical, while the query surface is graph-shaped.
44
+
45
+ ```text
46
+ UserPermissions table
47
+
48
+ Entity PK SK GSI1PK GSI1SK
49
+ Group GROUP#<groupId> META
50
+ User USER#<userId> PROFILE EMAIL#<email> PROFILE
51
+ GroupMembership GROUP#<groupId> USER#<userId> USER#<userId> GROUP#<groupId>
52
+ Permission GROUP#<groupId> PERM#<resource>#<action>
53
+ ```
54
+
55
+ `GroupMembership` uses the adjacency list pattern:
56
+
57
+ - `PK/SK` reads members of a group
58
+ - `GSI1PK/GSI1SK` reads groups for a user
59
+
60
+ The same physical table supports both directions without hiding the keys.
61
+
62
+ ```ts
63
+ const engGroup = await Group.query(
64
+ { groupId: 'eng' },
65
+ {
66
+ groupId: true,
67
+ name: true,
68
+ members: {
69
+ select: { userId: true, role: true },
70
+ limit: 20,
71
+ },
72
+ permissions: {
73
+ select: { resource: true, action: true },
74
+ limit: 20,
75
+ },
76
+ },
77
+ );
78
+
79
+ // Find a user by email (GSI), list their groups, and for each group fetch the
80
+ // group and its permissions. The N memberships -> parent Group resolution
81
+ // collapses into a single BatchGetItem (no N+1).
82
+ const alice = await User.query(
83
+ { email: 'alice@example.com' },
84
+ {
85
+ userId: true,
86
+ name: true,
87
+ groups: {
88
+ select: {
89
+ groupId: true,
90
+ role: true,
91
+ group: { // membership -> Group (belongsTo, BatchGet)
92
+ select: {
93
+ name: true,
94
+ permissions: { // group -> permissions (hasMany)
95
+ select: { resource: true, action: true },
96
+ filter: { resource: { attributeExists: true } },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ limit: 20,
102
+ },
103
+ },
104
+ { maxDepth: 3 },
105
+ );
106
+ ```
107
+
108
+ The first query expands into a small, inspectable DynamoDB plan:
109
+
110
+ ```text
111
+ 1. Query PK = GROUP#eng, SK = META -> Group
112
+ 2. Query PK = GROUP#eng, SK begins_with USER# -> members ┐ dispatched
113
+ 3. Query PK = GROUP#eng, SK begins_with PERM# -> permissions ┘ in parallel
114
+ ```
115
+
116
+ Independent relation sub-queries (here `members` and `permissions`) are dispatched concurrently; the `belongsTo` resolution in the second query batches N lookups into one `BatchGetItem`. See the [example](./examples/user-permissions/) for a timeline that proves the parallel dispatch.
117
+
118
+ ## Usage
119
+
120
+ The snippets below are compressed from the [user-permissions](./examples/user-permissions/) example.
121
+
122
+ ### Entity Definition
123
+
124
+ A TypeScript class is the single source of truth. Keys and GSIs are built from `k` tagged-template
125
+ **segments**, and the key builder's input type becomes the query parameter type.
126
+
127
+ ```ts
128
+ const TABLE = 'UserPermissions';
129
+
130
+ @model({ table: TABLE, prefix: 'USER' })
131
+ class UserModel extends DDBModel {
132
+ static readonly keys = key<{ userId: string }>((c) => ({
133
+ pk: k`USER#${c.userId}`,
134
+ sk: k`PROFILE`,
135
+ }));
136
+
137
+ static readonly emailIndex = gsi<{ email: string }>(
138
+ 'GSI1',
139
+ (c) => ({ pk: k`EMAIL#${c.email}`, sk: k`PROFILE` }),
140
+ { unique: true },
141
+ );
142
+
143
+ @string userId!: string;
144
+ @string name!: string;
145
+ @string email!: string;
146
+ @string status!: string;
147
+
148
+ @hasMany(() => GroupMembershipModel, { userId: 'userId' }, {
149
+ limit: { default: 20, max: 100 },
150
+ })
151
+ groups!: GroupMembershipModel[];
152
+ }
153
+
154
+ @model({ table: TABLE, prefix: 'GROUP' })
155
+ class GroupMembershipModel extends DDBModel {
156
+ static readonly keys = key<{ groupId: string; userId: string }>((c) => ({
157
+ pk: k`GROUP#${c.groupId}`,
158
+ sk: k`USER#${c.userId}`,
159
+ }));
160
+
161
+ static readonly userGroupsIndex = gsi<{ userId: string; groupId: string }>(
162
+ 'GSI1',
163
+ (c) => ({ pk: k`USER#${c.userId}`, sk: k`GROUP#${c.groupId}` }),
164
+ );
165
+
166
+ @string groupId!: string;
167
+ @string userId!: string;
168
+ @string role!: string;
169
+ }
170
+
171
+ export const User = UserModel.asModel();
172
+ export const GroupMembership = GroupMembershipModel.asModel();
173
+ ```
174
+
175
+ A multi-segment sort key is an array of segments (e.g. `sk: [k\`PERM#${c.resource}\`, k\`${c.action}\`]`);
176
+ a partial key compiles to `begins_with` at the matching segment boundary. See the
177
+ [specification](./docs/spec.md) for the full key model.
178
+
179
+ ### GraphQL-like Queries
180
+
181
+ The first argument describes how to find data.
182
+ The second argument describes what to return.
183
+ The runtime resolves which index to use from the provided field names.
184
+
185
+ ```ts
186
+ // Fetch a user by email and traverse to group memberships.
187
+ const alice = await User.query(
188
+ { email: 'alice@example.com' },
189
+ {
190
+ userId: true,
191
+ name: true,
192
+ groups: {
193
+ select: { groupId: true, role: true },
194
+ limit: 20,
195
+ },
196
+ },
197
+ );
198
+
199
+ // Unknown key field -> compile-time error
200
+ User.query({ foo: 'bar' }, { name: true });
201
+ ```
202
+
203
+ The return type is inferred from `select`:
204
+
205
+ ```ts
206
+ // alice: {
207
+ // name: string;
208
+ // groups: {
209
+ // items: { groupId: string; role: string }[];
210
+ // cursor: string | null;
211
+ // };
212
+ // }
213
+ ```
214
+
215
+ ### List Queries
216
+
217
+ ```ts
218
+ const result = await GroupMembership.list(
219
+ { groupId: 'eng' },
220
+ {
221
+ select: { userId: true, role: true },
222
+ limit: 20,
223
+ after: cursor,
224
+ },
225
+ );
226
+ // result.items: GroupMembership[]
227
+ // result.cursor: string | null
228
+ ```
229
+
230
+ ### Filtering (server-side `filter`)
231
+
232
+ `filter` is a declarative, type-safe condition compiled to a DynamoDB **`FilterExpression`** and
233
+ evaluated **server-side**. Modeled on AWS AppSync's `ModelFilterInput`: a bare value is an
234
+ equality shorthand, an operator object expresses comparisons, and `and` / `or` / `not` build
235
+ logical groups. It is typed against the **full entity**, so it can reference attributes that are
236
+ **not** in `select`.
237
+
238
+ ```ts
239
+ const result = await Order.list(
240
+ { userId: 'u001' },
241
+ {
242
+ select: { orderId: true }, // amount / status need not be projected
243
+ filter: {
244
+ status: 'confirmed', // #status = :v (equality shorthand)
245
+ amount: { gt: 100 }, // #amount > :v
246
+ title: { beginsWith: 'A' }, // begins_with(#title, :v)
247
+ shippedAt: { attributeExists: true },
248
+ or: [{ status: 'pending' }, { amount: { lt: 10 } }],
249
+ },
250
+ },
251
+ );
252
+ ```
253
+
254
+ Relations take the same `filter` via `Model.relation(...)` of the target model. For logic a
255
+ server-side `FilterExpression` cannot express, call `result.items.filter(...)` on the returned
256
+ projection — it keeps each selected field's declared scalar type, so `o.amount > 100` needs no
257
+ cast (there is no built-in post-load predicate).
258
+
259
+ Available operators (per field type): `eq`, `ne`, `ge`, `le`, `gt`, `lt`,
260
+ `between`, `in`, `beginsWith`, `contains`, `notContains`, `attributeExists`,
261
+ `attributeType`, `size`. Operators are **constrained by the field's declared
262
+ type** — e.g. `beginsWith` / `contains` on a numeric field, or `gt: 'x'` on a
263
+ number, are compile errors.
264
+
265
+ For rare conditions the operator objects cannot express, the `cond` raw escape
266
+ hatch accepts an expression fragment. Column names must use refactor-safe
267
+ `Model.col.<field>` references (never bare strings); values are parameterized:
268
+
269
+ ```ts
270
+ filter: cond`${Order.col.amount} > ${100} AND attribute_exists(${Order.col.status})`
271
+ ```
272
+
273
+ All values are parameterized (`ExpressionAttributeValues`) and all column names
274
+ are aliased (`ExpressionAttributeNames`) — there is no literal interpolation, so
275
+ the compiled expression is injection-free, and it is attached independently of
276
+ the `KeyConditionExpression` / `ProjectionExpression`.
277
+
278
+ > **RCU note:** A `FilterExpression` does **not** reduce read capacity —
279
+ > DynamoDB reads matching keys first, then filters. `limit` is applied
280
+ > **before** the filter, so a single page may return **fewer** than `limit`
281
+ > items (and, if the whole page is filtered out, an empty page with a non-null
282
+ > cursor). Design keys for efficient narrowing; use `filter` for correctness.
283
+
284
+ ### Writes
285
+
286
+ ```ts
287
+ await User.put({
288
+ userId: 'alice',
289
+ name: 'Alice',
290
+ email: 'alice@example.com',
291
+ status: 'active',
292
+ });
293
+
294
+ await GroupMembership.put({
295
+ groupId: 'eng',
296
+ userId: 'alice',
297
+ role: 'admin',
298
+ });
299
+
300
+ await User.update(alice, { status: 'disabled' });
301
+ ```
302
+
303
+ ### Inspect Execution Plans
304
+
305
+ `explain()` shows the DynamoDB operations before they are executed.
306
+ `select` is converted into a `ProjectionExpression`, so only requested attributes are read.
307
+
308
+ ```ts
309
+ const plan = GroupMembership.explain(
310
+ { groupId: 'eng' },
311
+ { select: { userId: true, role: true }, limit: 10 },
312
+ );
313
+ // {
314
+ // operations: [{
315
+ // type: "Query",
316
+ // tableName: "UserPermissions",
317
+ // keyCondition: { PK: "GROUP#eng" },
318
+ // rangeCondition: {
319
+ // operator: "begins_with",
320
+ // key: "SK",
321
+ // value: "USER#"
322
+ // },
323
+ // limit: 10
324
+ // }]
325
+ // }
326
+ ```
327
+
328
+ For partial GSI key matches, the SK condition is separated as a `rangeCondition`:
329
+
330
+ ```ts
331
+ const gsiPlan = GroupMembership.explain(
332
+ { userId: 'alice' },
333
+ { select: { groupId: true, role: true }, limit: 10 },
334
+ );
335
+ // {
336
+ // operations: [{
337
+ // type: "Query",
338
+ // keyCondition: { GSI1PK: "USER#alice" },
339
+ // indexName: "GSI1",
340
+ // rangeCondition: {
341
+ // operator: "begins_with",
342
+ // key: "GSI1SK",
343
+ // value: "GROUP#"
344
+ // },
345
+ // ...
346
+ // }]
347
+ // }
348
+ ```
349
+
350
+ ## Core Concepts
351
+
352
+ ### Entity = Access Pattern
353
+
354
+ An Entity is not an ORM model.
355
+ It is a bounded access surface for DynamoDB.
356
+
357
+ An Entity defines every supported way to access data within a service boundary:
358
+
359
+ - physical keys
360
+ - GSIs
361
+ - relations
362
+ - projections
363
+ - traversal rules
364
+
365
+ Physically, an Entity maps to a DynamoDB item.
366
+ Architecturally, it defines the allowed access surface for that part of the service.
367
+
368
+ ### Key / GSI = Structured Segments
369
+
370
+ `key()` and `gsi()` build keys from `k` tagged-template **segments**. The builder's input type
371
+ defines the fields required to resolve the key, and also becomes the query parameter type.
372
+
373
+ ```ts
374
+ // One definition serves two roles:
375
+ // 1. Write path: generate PK/SK when saving an Entity
376
+ // 2. Query path: define the parameter type for User.query({ email: ... })
377
+ static readonly emailIndex = gsi<{ email: string }>(
378
+ 'GSI1',
379
+ (c) => ({ pk: k`EMAIL#${c.email}`, sk: k`PROFILE` }),
380
+ { unique: true },
381
+ );
382
+ ```
383
+
384
+ Because keys are structured segments rather than opaque strings, a partial key — one supplying
385
+ only a leading prefix of the sort-key segments — compiles to `begins_with` at that segment
386
+ boundary. This is how relations and prefix queries resolve.
387
+
388
+ ### Relation = Bound Query
389
+
390
+ A Relation is a query bound to values from the parent Entity.
391
+ The same resolution logic applies: the runtime matches field names against Key / GSI definitions.
392
+
393
+ ```ts
394
+ @hasMany(() => GroupMembershipModel, { userId: 'userId' }, {
395
+ limit: { default: 20, max: 100 },
396
+ })
397
+ groups!: GroupMembershipModel[];
398
+ ```
399
+
400
+ ### Field Decorators
401
+
402
+ Type-safe semantic decorators define DynamoDB attribute types.
403
+ Compile-time checks based on `ClassFieldDecoratorContext` detect mismatches between declared TypeScript types and DynamoDB types.
404
+
405
+ ```ts
406
+ @string userId!: string; // DynamoDB S
407
+ @number amount!: number; // DynamoDB N
408
+ @datetime createdAt!: Date; // stored as an ISO 8601 string
409
+ @boolean isActive!: boolean; // DynamoDB BOOL
410
+ ```
411
+
412
+ ### Select = Projection + Type Inference
413
+
414
+ The `select` object controls both DynamoDB `ProjectionExpression` generation and TypeScript return type inference.
415
+ Only requested fields are returned, and only those fields appear in the result type.
416
+
417
+ ### Planner = Inspectable Execution Plan
418
+
419
+ Before DynamoDB is accessed, the runtime generates an execution plan.
420
+ Plans can be used for tests, debugging, and RCU estimation.
421
+
422
+ ## Architecture
423
+
424
+ ```text
425
+ Layer 1: Schema Definition
426
+ Entity / Field / Key / GSI / Relation / Embedded
427
+ -> EntityMetadata Registry
428
+
429
+ Layer 2: Query Builder
430
+ Type-safe DSL: key resolution, select validation
431
+ -> ExecutionPlan
432
+
433
+ Layer 3: Query Planner
434
+ Relation traversal, Get vs Query vs BatchGet decisions
435
+ Projection generation, GSI selection
436
+ -> DynamoDBOperation[]
437
+
438
+ Layer 4: Executor
439
+ DynamoDB DocumentClient wrapper
440
+ -> raw Items
441
+
442
+ Layer 5: Hydrator
443
+ raw Items -> Partial Entity instances
444
+ type conversion, Embedded reconstruction, metadata attachment
445
+ ```
446
+
447
+ ## Design Rules
448
+
449
+ | Rule | Description |
450
+ |------|-------------|
451
+ | Service Boundary First | One table represents one service boundary, not one entity. |
452
+ | Access Pattern First | Model DynamoDB access patterns before modeling tables or ORM-style entities. |
453
+ | Natural Mapping | Map Query / Get / BatchGet / Projection / cursor to a GraphQL-like query shape. |
454
+ | Inspectable Plans | DynamoDB operations and execution plans are inspectable before execution. |
455
+ | Single-item query | `query` is only for PK or unique GSI lookups. Use `list` for keys that can return multiple items. |
456
+ | Explicit limits | `hasMany` and `list` require default/max limits. |
457
+ | Bounded depth | Relation traversal defaults to depth=1. Deeper traversal must be explicitly allowed with `maxDepth`. |
458
+ | Parallel relations | Independent relation sub-queries, BatchGet chunks, and nested resolution are dispatched concurrently (bounded). DynamoDB is HTTP-based, so there is no connection to serialize on. |
459
+ | No Scan | Full table scans are forbidden. The linter detects and blocks them. |
460
+ | Partial select | Query results are partial types. Only selected fields appear in the result type. |
461
+ | Structured keys | Keys are built from `k` segment templates; partial keys compile to `begins_with` at a segment boundary. |
462
+ | Typed consistentRead | `consistentRead` is only available for PK queries. GSI queries are rejected at compile time. |
463
+ | Server-side filter | `filter` is a declarative DynamoDB `FilterExpression` (AppSync `ModelFilterInput`-compatible), typed against the full entity and able to reference unprojected attributes. It does not reduce RCU, and `limit` is applied before it. |
464
+ | Post-load filtering | No built-in post-load predicate; use `result.items.filter(...)` on the typed projection. Efficient narrowing belongs in key design. |
465
+
466
+ ## Architectural Boundaries
467
+
468
+ Relation traversal only follows access paths defined on Entities.
469
+ Undefined relations, missing GSIs, lists without limits, and traversal that exceeds allowed depth are checked before execution.
470
+
471
+ This makes cross-boundary queries and DynamoDB-unfriendly access patterns easier to see in code.
472
+ The same architectural constraints apply regardless of whether code is written by humans or generated by AI.
473
+
474
+ ## Linter
475
+
476
+ Static analysis detects problems in Entity definitions before runtime:
477
+
478
+ - Scan usage
479
+ - missing limits on `hasMany` / `list`
480
+ - ambiguous GSI resolution
481
+ - query/list boundary violations for unique GSIs
482
+
483
+ ## Beyond the Core
484
+
485
+ The query / relation / write API above is the core layer. GraphDDB also ships:
486
+
487
+ - **[CQRS contracts](./docs/cqrs-contract.md)** — public Query/Command contract models with an
488
+ N+1-safe cardinality matrix, cross-contract composition, and context-boundary enforcement.
489
+ - **[Mutation → command derivation](./docs/mutation-command-derivation.md)** — an internal
490
+ write-plan composition DSL behind the public Command IF. Declare a model's write semantics
491
+ (`entityWrites`: referential integrity, uniqueness, edge effects, derived counters, outbox
492
+ events, idempotency) once; a `mutation` composes one or more write fragments that the compiler
493
+ merges into a single atomic `TransactWriteItems`. All write paths route through one shared
494
+ `commitTransaction` orchestration.
495
+ - **[Opt-in class hydration](./docs/class-hydration.md)** — pass an `options.hydrate` factory to
496
+ load a read result onto a host-language domain object instead of the default Typed Plain Object.
497
+ Host-only and never serialized into the bridge SSoT. Phase 1 (`query` top-level) is implemented;
498
+ `list` / per-relation hydration are planned future phases.
499
+ - **[Multi-language (Python bridge)](./docs/python-bridge.md)** — TypeScript is the single source
500
+ of truth; generate a Python client + runtime from the same definitions, kept in lockstep by a
501
+ TS↔Python conformance suite.
502
+ - **[In-memory testing](./docs/testing.md)** — `graphddb/testing` runs model-mapping, query-plan,
503
+ relation-traversal, and CDC tests **in-process, without Docker**.
504
+ - **[CDC emulator](./docs/cdc-emulator.md)** — DynamoDB-Streams-equivalent change events for
505
+ driving and testing incremental aggregation locally.
506
+
507
+ ## Documentation
508
+
509
+ | Document | Description |
510
+ |----------|-------------|
511
+ | [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transactions, design rules, runtime behavior. |
512
+ | [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, cross-contract composition, context boundaries. |
513
+ | [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF: model write-semantics (`entityWrites`), fragment composition, and atomic `TransactWriteItems` derivation. |
514
+ | [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` factory that loads a read result onto a host-language object (Phase 1: `query` top-level; Phases 2–3 future). |
515
+ | [Multi-language / Python bridge](./docs/python-bridge.md) | TS-as-SSoT code generation and the Python runtime; TS↔Python conformance. |
516
+ | [In-memory test adapter](./docs/testing.md) | Docker-free, in-process testing via `graphddb/testing` and `MemoryInspector`. |
517
+ | [CDC emulator](./docs/cdc-emulator.md) | Change-event emulator (dev/test) for incremental aggregation patterns. |
518
+ | [Benchmark](./benchmark/RESULTS.md) | Library comparison vs. hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |
519
+
520
+ ## Examples
521
+
522
+ | Example | Description |
523
+ |---------|-------------|
524
+ | [user-permissions](./examples/user-permissions/) | Manage users, groups, and permissions with Single Table Design. Includes adjacency lists, inverted indexes, relation traversal, and `explain`. |
525
+ | [aggregate-tree-pattern](./examples/aggregate-tree-pattern/) | Incremental tree aggregation (`siteScoreAverage`) driven by the CDC emulator: dirty propagation, throttled sweep, and recompute. |
526
+
527
+ ## Features
528
+
529
+ - Entity metadata / decorators
530
+ - DDBModel base class / connection management
531
+ - Type system (SelectableOf, QueryResult, QueryKeyOf)
532
+ - CRUD (put, update, delete, conditional writes)
533
+ - Query / List (planner, executor, hydrator, cursor pagination)
534
+ - Type-safe filtering: declarative server-side `filter` (FilterExpression / AppSync `ModelFilterInput`-compatible), plus `Model.col` column refs and the `cond` raw escape hatch
535
+ - Structured segment keys (`k` tag): partial keys compile to `begins_with` at a segment boundary
536
+ - Explainable execution plans
537
+ - Relations (hasMany, belongsTo, hasOne, depth limits, traversal)
538
+ - BatchGet optimization
539
+ - Parallel relation execution (independent sub-queries, BatchGet chunks, and nested resolution are dispatched concurrently with bounded concurrency)
540
+ - Linter (no-scan, require-limit, query-boundary, GSI ambiguity, expanded design rules)
541
+ - ConsistentRead type constraints
542
+ - Transaction DSL
543
+ - Batch operations
544
+ - CQRS Query/Command contract layer (public IF, N+1-safe composition, context boundaries) — see [docs](./docs/cqrs-contract.md)
545
+ - Mutation → command derivation: internal write-plan composition DSL (`entityWrites` + `mutation`) behind the Command IF, multi-fragment atomic composition into one `TransactWriteItems` — see [docs](./docs/mutation-command-derivation.md)
546
+ - Opt-in class hydration (`options.hydrate`): load a read result onto a host object; host-only, never serialized. Phase 1 (`query` top-level) implemented — see [docs](./docs/class-hydration.md)
547
+ - Multi-language: TS-as-SSoT Python code generator + runtime, with TS↔Python conformance — see [docs](./docs/python-bridge.md)
548
+ - In-memory test adapter (`graphddb/testing`): Docker-free unit testing — see [docs](./docs/testing.md)
549
+ - CDC emulator: DynamoDB-Streams-equivalent change events for dev/test — see [docs](./docs/cdc-emulator.md)
550
+
551
+ ## License
552
+
553
+ MIT