graphddb 0.7.10 → 0.8.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.
Files changed (46) hide show
  1. package/README.md +6 -6
  2. package/dist/cdc/index.d.ts +389 -5
  3. package/dist/cdc/index.js +4 -4
  4. package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
  5. package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
  6. package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
  7. package/dist/chunk-L4QRCHRQ.js +278 -0
  8. package/dist/chunk-LAT64YCZ.js +1987 -0
  9. package/dist/chunk-S2NI4PBW.js +187 -0
  10. package/dist/{chunk-EOJDN3SA.js → chunk-T44OB5GU.js} +3757 -6138
  11. package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
  12. package/dist/cli.js +63 -254
  13. package/dist/index.d.ts +23 -1550
  14. package/dist/index.js +94 -1850
  15. package/dist/internal/index.d.ts +84 -0
  16. package/dist/internal/index.js +701 -0
  17. package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DZtjAQDh.d.ts} +573 -1817
  18. package/dist/linter/index.d.ts +39 -7
  19. package/dist/linter/index.js +22 -4
  20. package/dist/{registry-LWE54Sdc.d.ts → linter-DQY7gUEk.d.ts} +22 -22
  21. package/dist/prepared-artifact-HFealr1q.d.ts +281 -0
  22. package/dist/spec/index.d.ts +506 -5
  23. package/dist/spec/index.js +22 -18
  24. package/dist/testing/index.d.ts +2 -3
  25. package/dist/testing/index.js +4 -4
  26. package/dist/transform/index.d.ts +1 -1
  27. package/dist/transform/index.js +4 -4
  28. package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
  29. package/dist/types-DW__-Icc.d.ts +450 -0
  30. package/docs/cdc-projection.md +5 -5
  31. package/docs/class-hydration.md +1 -1
  32. package/docs/cqrs-contract.md +79 -20
  33. package/docs/design-patterns.md +5 -5
  34. package/docs/docs-generation.md +6 -6
  35. package/docs/middleware.md +15 -15
  36. package/docs/mutation-command-derivation.md +52 -42
  37. package/docs/prepared-statements.md +14 -14
  38. package/docs/python-bridge.md +96 -65
  39. package/docs/spec.md +153 -124
  40. package/docs/testing.md +9 -8
  41. package/package.json +14 -4
  42. package/dist/chunk-3UD3XIF2.js +0 -860
  43. package/dist/chunk-MMVHOUM4.js +0 -24
  44. package/dist/from-change-Ty95KA8C.d.ts +0 -327
  45. package/dist/index-Dc7d8mWI.d.ts +0 -1089
  46. package/dist/relation-depth-BRS513Tq.d.ts +0 -36
@@ -83,7 +83,9 @@ field list argument. The access pattern — primary key or GSI — is resolved f
83
83
  the `Model` plus the `key` fields named in the descriptor:
84
84
 
85
85
  ```ts
86
- const UserByEmail = publicQueryModel({
86
+ import { graphddb, param } from 'graphddb';
87
+
88
+ const UserByEmail = graphddb.publishQuery({
87
89
  get: { query: User, key: { email: param.string() }, select: { userId: true, email: true } },
88
90
  });
89
91
  ```
@@ -95,7 +97,7 @@ key, select, options? }`, with exactly one of `query` / `list`. `select` and
95
97
  `options` are separate keys (no `select` nested inside `options`).
96
98
 
97
99
  ```ts
98
- export const ArticleById = publicQueryModel({
100
+ export const ArticleById = graphddb.publishQuery({
99
101
  get: { query: Article, key: { articleId: param.string() },
100
102
  select: { articleId: true, title: true, body: true },
101
103
  options: { consistentRead: false } },
@@ -193,7 +195,7 @@ write-use-case Methods. A command method is a single declarative write
193
195
  result?, mode? }`, with exactly one of `create` / `update` / `remove`.
194
196
 
195
197
  ```ts
196
- export const UserCommands = publicCommandModel({
198
+ export const UserCommands = graphddb.publishCommand({
197
199
  disable: { update: User, key: { userId: param.string() },
198
200
  input: { status: 'disabled', disableReason: param.string() },
199
201
  result: { select: { userId: true, status: true } } },
@@ -214,7 +216,7 @@ export const UserCommands = publicCommandModel({
214
216
  **or** a raw `cond\`…\`` fragment whose value slots may be `param.*` bindings:
215
217
 
216
218
  ```ts
217
- export const UserCommands = publicCommandModel({
219
+ export const UserCommands = graphddb.publishCommand({
218
220
  // declarative operators in a write condition
219
221
  promote: { update: User, key: { userId: param.string() },
220
222
  input: { role: 'admin' },
@@ -261,6 +263,52 @@ addMany: { create: GroupMembership, key: { groupId: param.string(), userId: para
261
263
  `GraphQL contrast`: `mode: 'transaction'` is atomic all-or-nothing; `mode:
262
264
  'parallel'` is non-atomic per-field partial success.
263
265
 
266
+ ### Composite (multi-entity atomic) methods — the `writes` body
267
+
268
+ A single write descriptor covers one entity. A **composite** method writes
269
+ **multiple entities atomically** — the canonical case being a cross-entity write
270
+ that has **no relation to fall back on** (e.g. marking a hash-keyed aggregate node
271
+ `dirty` alongside a chunk's terminal update). Author it with the **`writes` body**
272
+ form:
273
+
274
+ ```ts
275
+ export const ChunkCompleteCommands = graphddb.publishCommand({
276
+ complete: {
277
+ // the public input schema — names + types the params the body references
278
+ input: { chunkId: param.string(), nodeId: param.string() },
279
+ // the mutation body: `$ => ({ alias: descriptor, … })`; `$` refers to the inputs
280
+ writes: ($) => ({
281
+ chunk: { update: Chunk, key: { chunkId: $.chunkId }, input: { done: true } },
282
+ agg: { update: AggNode, key: { nodeId: $.nodeId }, input: { dirty: true } },
283
+ }),
284
+ // optional read-back projection (keyed on the primary — first — fragment)
285
+ result: { select: { chunkId: true, done: true } },
286
+ },
287
+ });
288
+ ```
289
+
290
+ - `writes` is a body `$ => ({ alias: writeDescriptor, … })`. Each alias value is a
291
+ write descriptor (`{ create | update | remove | upsert: Model, key, input?,
292
+ condition? }` — the same shape as a single-descriptor method, minus `result` /
293
+ `mode`, which are declared on the composite as a whole). The `$` proxy fields are
294
+ the input fields declared in the separate **`input`** schema; a later descriptor
295
+ may reference an earlier alias's written field via `$.alias.field`.
296
+ - **Single-alias** `writes` ⇒ one write. **Multi-alias** ⇒ an **atomic composite**:
297
+ it is promoted to a single `TransactWriteItems` by DynamoDB necessity (there is no
298
+ non-atomic multi-item form for a cross-entity write), so no `mode` ceremony is
299
+ needed — a multi-alias composite is always atomic.
300
+ - `input` (the public param schema), `result` (read-back), and `mode` are specified
301
+ **alongside** the body, never baked into it.
302
+ - **No `mutation(...)` / `defineScpTransaction(...)` import is needed** —
303
+ `publishCommand` compiles the `writes` body internally. The emitted
304
+ `operations.json` wire is byte-identical to the equivalent internal composition.
305
+ - The single-descriptor form above stays the sugar for a single-entity write; the
306
+ `writes` body is the additive form for the multi-entity atomic case.
307
+
308
+ > The full **SCP native-syntax** composite body (imperative `$`/`?:`/`&&`/`.map`
309
+ > transaction sources) is a separate surface, deferred to a later release; the
310
+ > `writes` descriptor-map body above is the supported public composite form today.
311
+
264
312
  #### The 25-item limit is a hard error, never a silent split
265
313
 
266
314
  `TransactWriteItems` is capped at **25 items** and is atomic. A
@@ -276,7 +324,7 @@ read dependency, declared with the `query($ => ({...}))` builder; a cross-
276
324
  fragment reference uses `$.alias.field`:
277
325
 
278
326
  ```ts
279
- export const AccountAccess = publicQueryModel({
327
+ export const AccountAccess = graphddb.publishQuery({
280
328
  get: query($ => ({
281
329
  account: { query: Account, key: { accountId: param.string() },
282
330
  select: { accountId: true } },
@@ -361,7 +409,8 @@ single-model lint framework.
361
409
 
362
410
  A method body is evaluated at definition time with throwing sentinels in place
363
411
  of `keys` / `params`, recorded through a model recorder, and subjected to the
364
- same hardening `defineTransaction` applies:
412
+ same no-runtime-capture build-time hardening the prepared-statement compiler
413
+ applies:
365
414
 
366
415
  1. **Throwing sentinel Proxy** — any property / method access other than the
367
416
  internal brand reads or primitive coercion throws at the access site.
@@ -489,31 +538,41 @@ input produces a byte-identical pre-contract document).
489
538
 
490
539
  ## Public API surface
491
540
 
492
- Definition DSL (`src/define/contract.ts`):
541
+ Definition DSL (`src/define/contract.ts`) — import the `graphddb` namespace
542
+ (`import { graphddb } from 'graphddb'`); `graphddb.publishQuery` /
543
+ `graphddb.publishCommand` are the sole read/write authoring surface:
493
544
 
494
545
  ```ts
495
- publicQueryModel({ alias: { query | list: Model, key, select, options? } })
496
- publicCommandModel({ alias: { create | update | remove: Model, key, input, condition?, result?, mode? } })
497
- query($ => ({ alias: descriptor, ... })) // composite read; cross-fragment refs $.alias.field
498
- mutation($ => ({ alias: descriptor, ... })) // composite write; cross-fragment refs $.alias.field
546
+ graphddb.publishQuery({ alias: { query | list: Model, key, select, options? } })
547
+ // single-entity write
548
+ graphddb.publishCommand({ alias: { create | update | remove: Model, key, input, condition?, result?, use?, mode? } })
549
+ // multi-entity atomic (composite) write the `writes` body form (#293)
550
+ graphddb.publishCommand({ alias: { input, writes: $ => ({ alias: descriptor, … }), result?, mode? } })
499
551
  param.string() / param.number() / ... // descriptor key/input parameters
500
552
  ```
501
553
 
502
- Runtime (`src/runtime/*`):
554
+ A query method may compose another query contract's method (cross-fragment refs
555
+ via `$.alias.field`); a command method composes its derived write ops and
556
+ maintenance writes into one plan, and a **composite** command method's `writes`
557
+ body composes a multi-entity atomic write (§ Composite methods). These are all
558
+ declared inside the `publishQuery` / `publishCommand` map — there is no separate
559
+ public `query()` / `mutation()` composite verb (removed in 0.8.0; the composite
560
+ `writes` body is compiled internally, needing no import).
503
561
 
504
- ```ts
505
- executeQueryMethod(contract, methodName, key | keys[], params?) // contract-runtime
506
- executeCommandMethod(contract, methodName, key | keys[], params?) // command-runtime
507
- encodePerKeyCursor / decodePerKeyCursor / serializeContractKey // per-key-cursor
508
- ```
562
+ Execution is the two `graphddb.*` verbs (`graphddb.query` / `graphddb.mutate`) —
563
+ they route contract methods through the in-process runtime.
564
+
565
+ The contract runtime (`executeQueryMethod` / `executeCommandMethod` /
566
+ per-key-cursor helpers, `src/runtime/*`) is **internal** machinery behind those
567
+ verbs — not a public export.
509
568
 
510
- Build / static analysis (`src/spec/*`):
569
+ Build / static analysis is the advanced `graphddb/spec` subpath:
511
570
 
512
571
  ```ts
572
+ import { buildContracts, buildContexts } from 'graphddb/spec';
573
+
513
574
  buildContracts(contracts) // → { contracts, queries, commands, transactions }
514
575
  buildContexts(contexts) // → context-ownership specs
515
- assertContractN1Safe(name, spec, isGsiPoint?)
516
- assertContractBoundaries(...) // context-boundary lint
517
576
  ```
518
577
 
519
578
  ## Query vs Command
@@ -542,11 +542,11 @@ GSI). *Dual-edge form* — declare `w.dualEdge(forward, inverse)` on the forward
542
542
  keep a **two-row** bidirectional edge in sync without an inverse GSI. The dual edge can
543
543
  be recorded in either of two write vocabularies carrying the SAME `w.dualEdge`:
544
544
 
545
- - **`entityWrites` (recommended when driving through the command / mutation IF, #195)** —
545
+ - **`entityWrites` (recommended when driving through the command IF, #195)** —
546
546
  place `w.dualEdge(...)` in a lifecycle's `edges` array. The dual edge is then derived by
547
- `executeCommandMethod` / `DDBModel.mutate`, so a `mutation` over the edge maintains BOTH
548
- rows in the SAME atomic transaction with no low-level handwork. Use this form if the edge
549
- is created / removed through a command.
547
+ the command runtime behind `graphddb.publishCommand` / `graphddb.mutate`, so a write over
548
+ the edge maintains BOTH rows in the SAME atomic transaction with no low-level handwork. Use
549
+ this form if the edge is created / removed through a command.
550
550
  - **`edgeWrites` (the low-level edge-only primitive)** — the historical adjacency-only
551
551
  `writes` member. Same two-row synchronization, but driven through the low-level edge-write
552
552
  path rather than the command IF.
@@ -626,7 +626,7 @@ actual sink write happens inside the consumer's CDC infrastructure, outside grap
626
626
  That runtime was **removed in 0.4.0** (issue #152). External projection is now a
627
627
  **typed-consumer-IF contract** (issue #153): mark the source model with
628
628
  `@cdcProjected()`, then parse a CDC change event into typed instances — `Model.fromChange(event)`
629
- returns `[oldRecord, newRecord]`, and `DDBModel.subscribe(handlers)` (the GraphQL-style
629
+ returns `[oldRecord, newRecord]`, and `graphddb.subscribe(handlers)` (the GraphQL-style
630
630
  sibling of `query` / `mutate`) returns a `ChangeHandler` the consumer mounts on its own
631
631
  stream. graphddb owns the *parse → typed record* contract; subscription, sink delivery,
632
632
  and idempotency stay with the consumer. See [`cdc-projection.md`](./cdc-projection.md)
@@ -40,15 +40,15 @@ graphddb generate docs --entry models.ts --out doc.md
40
40
  `<dir>/index.md` (backward compatible). Either way the command prints a JSON
41
41
  result (`{"status":"ok","outDir":…,"files":[…]}`) to stdout, where `files`
42
42
  reports the written path(s).
43
- - **`--queries`** (optional) points at the module exporting your `queries` /
44
- `commands` (the `defineQueries` / `defineCommands` maps). It powers the Access
45
- Patterns and Mutation Contracts sections; omit it and those sections are empty.
46
- Defaults to the entry module.
43
+ - **`--contracts`** (optional) points at the module exporting your `contracts`
44
+ map (the `graphddb.publishQuery` / `graphddb.publishCommand` results). It powers
45
+ the Access Patterns and Mutation Contracts sections; omit it and those sections
46
+ are empty. Defaults to the entry module.
47
47
 
48
48
  To also document your access patterns and mutations:
49
49
 
50
50
  ```bash
51
- graphddb generate docs --entry models.ts --queries definitions.ts --out docs/
51
+ graphddb generate docs --entry models.ts --contracts contracts.ts --out docs/
52
52
  ```
53
53
 
54
54
  ## Output sections
@@ -78,7 +78,7 @@ flag (PITR, table class, encryption) render as `as configured at deployment`.
78
78
  | --- | --- | --- |
79
79
  | `--entry <file>` | (required) | Entry module registering the entity models. |
80
80
  | `--out <path>` | (required) | Output path. A `*.md` file is written verbatim at that path; any other value is a directory and `index.md` is written inside it. |
81
- | `--queries <file>` | entry module | Module exporting `queries` / `commands` for Access Patterns / Mutation Contracts. |
81
+ | `--contracts <file>` | entry module | Module exporting the `contracts` map for Access Patterns / Mutation Contracts. |
82
82
  | `--format <format>` | `markdown` | Output format (only `markdown` today). |
83
83
  | `--split <mode>` | `none` | `none` emits a single `index.md`. `entity` is reserved for a future per-entity split. |
84
84
  | `--template-dir <dir>` | — | Directory of override `<partial>.hbs` templates. |
@@ -25,10 +25,10 @@ without embedding any of it in your query or model definitions.
25
25
  ## Registration
26
26
 
27
27
  ```ts
28
- import { DDBModel } from 'graphddb';
28
+ import { graphddb } from 'graphddb';
29
29
 
30
- DDBModel.use(middleware); // register (call as many times as you like)
31
- DDBModel.clearMiddleware(); // remove all (e.g. in test teardown)
30
+ graphddb.config.use(middleware); // register (call as many times as you like)
31
+ graphddb.config.clearMiddleware(); // remove all (e.g. in test teardown)
32
32
  ```
33
33
 
34
34
  Each registered object may set any subset of read and/or write hooks. Order
@@ -45,8 +45,8 @@ every relation fan-out operation. A call with no `context` sees `{}`.
45
45
 
46
46
  ```ts
47
47
  await User.query(key, select, { context: { tenantId, actor } });
48
- await User.put(item, { context: { actor } });
49
- await DDBModel.batchGet(requests, { context: { tenantId } });
48
+ await User.putItem(item, { context: { actor } });
49
+ await graphddb.query({ users: { query: User, key: keys, select: { name: true } } }, { context: { tenantId } });
50
50
  ```
51
51
 
52
52
  `context` is an ordinary host-language value and is **never** serialized.
@@ -102,9 +102,9 @@ Context types: `ReadRequestCtx` (R1/R4/R5) carries `kind: 'query' | 'list' |
102
102
  `relationPath` (`[]` for the root read, `['orders']` for a direct relation,
103
103
  `['orders', 'product']` for a nested one).
104
104
 
105
- ### `DDBModel.batchGet`
105
+ ### Batch read (`graphddb.query({ key: K[] })`)
106
106
 
107
- The multi-key read primitive fires the same hooks: R1/R4/R5 with kind
107
+ The multi-key read path fires the same hooks: R1/R4/R5 with kind
108
108
  `'batchGet'` (the mutable batch input is `ctx.params.requests`), and R2/R3/R5 per
109
109
  underlying `BatchGetItem` op (one per ≤100-key chunk). Note DynamoDB
110
110
  `BatchGetItem` has no server-side `FilterExpression`, so an R2 hook scopes a batch
@@ -130,7 +130,7 @@ an optional `transaction.id` shared across the ops of one atomic batch.
130
130
  `PersistCtx` (W3/W4/W5) carries the mutable `items` and the `origins` of the
131
131
  batch.
132
132
 
133
- Atomicity: in `DDBModel.transaction` / `mutate({ mode: 'transaction' })`, persist
133
+ Atomicity: in `graphddb.mutate({ mode: 'transaction' })`, persist
134
134
  hooks fire **once** for the whole atomic batch and any `before` `throw` aborts the
135
135
  entire transaction; in `parallel` mode each op is independent.
136
136
 
@@ -148,26 +148,26 @@ entire transaction; in `parallel` mode each op is independent.
148
148
 
149
149
  ```ts
150
150
  // Tenant scope on every read, including relation fan-out (R2)
151
- DDBModel.use({ read: { op: { before(ctx) {
151
+ graphddb.config.use({ read: { op: { before(ctx) {
152
152
  addFilter(ctx.operation, 'tenantId', ctx.context.tenantId);
153
153
  } } } });
154
154
 
155
155
  // Inject audit fields on every write (W1) — flows through derivation
156
- DDBModel.use({ write: { before(ctx) {
156
+ graphddb.config.use({ write: { before(ctx) {
157
157
  if (ctx.kind !== 'delete') ctx.input.item = { ...ctx.input.item, updatedBy: ctx.context.actor };
158
158
  } } });
159
159
 
160
160
  // Soft delete by rewrite (W1) — stored as an update
161
- DDBModel.use({ write: { before(ctx) {
161
+ graphddb.config.use({ write: { before(ctx) {
162
162
  if (ctx.kind === 'delete') { ctx.kind = 'update'; ctx.input.changes = { deletedAt: Date.now() }; }
163
163
  } } });
164
164
 
165
165
  // Redact non-admin reads (R4) and audit the real atomic batch (W3)
166
- DDBModel.use({ read: { afterFetch: (ctx, r) => ctx.context.actor?.isAdmin ? r : redact(r) } });
167
- DDBModel.use({ write: { persist: { before(ctx) { auditLog(ctx.items, ctx.context); } } } });
166
+ graphddb.config.use({ read: { afterFetch: (ctx, r) => ctx.context.actor?.isAdmin ? r : redact(r) } });
167
+ graphddb.config.use({ write: { persist: { before(ctx) { auditLog(ctx.items, ctx.context); } } } });
168
168
 
169
169
  // Latency metric using per-request scratch (R1 → R4) and metrics on error (R5)
170
- DDBModel.use({ read: {
170
+ graphddb.config.use({ read: {
171
171
  before(ctx) { ctx.state.t0 = performance.now(); },
172
172
  afterFetch(ctx, r) { metric(`read.${ctx.kind}`, performance.now() - (ctx.state.t0 as number)); return r; },
173
173
  onError(ctx) { count(`read.${ctx.kind}.error`); /* return nothing → rethrow */ },
@@ -178,7 +178,7 @@ DDBModel.use({ read: {
178
178
 
179
179
  The Python runtime exposes the same hook points (R1–R5, W1–W5), interface shape,
180
180
  ordering, atomicity, `context` injection, and `onError` recovery, registered
181
- through the Python runtime's equivalent of `DDBModel.use`. Because hooks are
181
+ through the Python runtime's equivalent of `graphddb.config.use`. Because hooks are
182
182
  host-only and never serialized, the two runtimes implement them independently
183
183
  with no shared serialized surface; TS↔Python conformance is unaffected.
184
184
 
@@ -65,12 +65,12 @@ On top of that, the write-semantics vocabulary is declared additively (it does
65
65
 
66
66
  ### Command derivation, not hand-authored ops
67
67
 
68
- Earlier, the contract command layer (`publicCommandModel`, issue #64) had the
68
+ Earlier, the contract command layer (`graphddb.publishCommand`, issue #64) had the
69
69
  author *write* the operation: a method body resolved to a single `put` /
70
- `update` / `delete` (`CommandSpec`) or a hand-built `defineTransaction`
71
- (`TransactWriteItems`). The command compiler now derives the plan from declared
72
- write semantics: an **intent → derive ops** step exists, turning the procedural
73
- DSL into a *derivable contract*.
70
+ `update` / `delete` (`CommandSpec`) or a hand-built atomic `TransactWriteItems`.
71
+ The command compiler now derives the plan from declared write semantics: an
72
+ **intent → derive ops** step exists, turning the procedural DSL into a
73
+ *derivable contract*.
74
74
 
75
75
  ---
76
76
 
@@ -91,8 +91,9 @@ existing `CommandSpec` / `TransactionSpec` / `executionPlan` SSoT:
91
91
  | idempotency | client-token guard item (`attribute_not_exists`) | tx + idempotency declaration |
92
92
 
93
93
  A multi-effect create (item + edge + counter + uniqueness + event) is one
94
- **atomic `TransactWriteItems`** — exactly what `defineTransaction` serializes,
95
- with the new pieces being *declarations* the compiler turns into items. The
94
+ **atomic `TransactWriteItems`** — exactly what `graphddb.mutate({ mode:
95
+ 'transaction' })` serializes, with the new pieces being *declarations* the
96
+ compiler turns into items. The
96
97
  ≤25-item limit and the "no per-item condition in `BatchWriteItem`" rules (#64)
97
98
  apply unchanged.
98
99
 
@@ -119,7 +120,7 @@ Two capabilities the derivation depends on, both now implemented:
119
120
  `mutation` is an **internal DSL for declaring a set of updates that must execute
120
121
  atomically** — a *write-plan composition language* used as the *implementation*
121
122
  of a Command. The only externally-exposed surface is the fixed **Command IF**
122
- (`publicCommandModel`, #64): params in, result out.
123
+ (`graphddb.publishCommand`, #64): params in, result out.
123
124
 
124
125
  ```
125
126
  Public Command IF (external, fixed) = what other services depend on. ← the only public surface
@@ -234,34 +235,41 @@ override with a custom write contract — and it takes the whole `writes` set,
234
235
  never `writes.create` (that would double-specify the intent). A cross-fragment
235
236
  data dependency is a `$.alias.field` reference. The compiler merges ALL
236
237
  fragments into ONE atomic plan. The **public surface is the Command IF**
237
- (`publicCommandModel`, #64), whose method is *implemented by* the mutation.
238
+ (`graphddb.publishCommand`, #64), whose method is *implemented by* the mutation.
238
239
 
239
- ```ts
240
- // ── internal write-plan composition (NOT public) ──
241
- const CreatePost = mutation($ => ({
242
- // The INTENT (`create`) already selects the lifecycle. `use:` is OPTIONAL and
243
- // defaults to the target's own `writes`; the compiler picks writes.create for
244
- // create / writes.update for update / writes.remove for remove.
245
- post: { create: PostModel, key: { postId: param.string() }, // fragment 1 → PostModel.writes.create
246
- input: { userId: param.string(), title: param.string(), body: param.string() } },
247
- audit: { create: AuditLogModel, key: { id: param.string() }, // fragment 2 — composed atomically
248
- input: { action: 'post.created', actorId: $.post.userId, postId: $.post.postId } },
249
- }));
240
+ The public surface is `graphddb.publishCommand` — a map of method name → single
241
+ declarative write **descriptor** (`{ create | update | remove: Model, key, input?,
242
+ condition?, result?, use?, mode? }`). The INTENT (`create` / `update` / `remove`)
243
+ selects the lifecycle from the target's `writes`; the compiler folds the derived
244
+ effects (edges, counters, uniqueness guards, outbox, idempotency, and any
245
+ composed maintenance/audit writes) into ONE atomic `TransactWriteItems`. The
246
+ author writes only the descriptor:
250
247
 
251
- // Override only when a non-default save contract is wanted:
252
- // post: { create: PostModel, use: CustomPostWrites, key: {...}, input: { … } }
253
- // `use:` takes the WHOLE writes set (not writes.create) — the intent picks the
254
- // lifecycle, so writing `use: PostModel.writes.create` would double-specify it.
248
+ ```ts
249
+ import { graphddb, param } from 'graphddb';
255
250
 
256
251
  // ── public Command IF (external, fixed — the only public surface) ──
257
- export const PostCommands = publicCommandModel({
258
- create: mutation($ => ({
259
- post: { create: PostModel, key: { postId: param.string() },
260
- input: { requestId: param.string().optional(), userId: param.string(),
261
- title: param.string(), body: param.string() },
262
- result: { select: { postId: true, title: true } } }, // return = read projection
263
- })),
252
+ export const PostCommands = graphddb.publishCommand({
253
+ // Intent `create` selects PostModel.writes.create; the descriptor's derived
254
+ // effects + any composed maintenance/audit writes are merged into one atomic
255
+ // TransactWriteItems by the compiler.
256
+ create: {
257
+ create: PostModel,
258
+ key: { postId: param.string() },
259
+ input: {
260
+ requestId: param.string().optional(),
261
+ userId: param.string(),
262
+ title: param.string(),
263
+ body: param.string(),
264
+ },
265
+ result: { select: { postId: true, title: true } }, // return = read projection
266
+ },
264
267
  });
268
+
269
+ // Override the default save contract only when a non-default one is wanted:
270
+ // create: { create: PostModel, use: CustomPostWrites, key: {...}, input: { … } }
271
+ // `use:` takes the WHOLE writes set (not writes.create) — the intent picks the
272
+ // lifecycle, so writing `use: PostModel.writes.create` would double-specify it.
265
273
  ```
266
274
 
267
275
  Compilation (`compileFragment` for a single fragment;
@@ -283,18 +291,20 @@ Mutation = { alias₁: fragment₁, alias₂: fragment₂, … } (each: intent
283
291
  → attach executionPlan (#70); resolve return selection as a read projection
284
292
  ```
285
293
 
286
- At runtime, every write path — single command, multi-fragment mutation, and the
287
- declarative `defineTransaction` — routes through the **single shared
288
- `commitTransaction` orchestration** (`src/runtime/transaction-commit.ts`, #97):
289
- op-aware collapse of same-physical-key collisions, the ≤25 enforcement, the
290
- atomic commit, and the CDC write-capture loop all live in that one place, so no
291
- caller carries its own collapse / limit / commit / capture logic.
292
-
293
- Naming: `mutation($ => ({...}))` = the INTERNAL composition DSL (not public);
294
- `publicCommandModel({ alias: mutation(...) })` = the public Command IF;
294
+ At runtime, every write path — a single command, a multi-effect composed
295
+ descriptor, and an atomic `graphddb.mutate({ mode: 'transaction' })` — routes
296
+ through the **single shared `commitTransaction` orchestration**
297
+ (`src/runtime/transaction-commit.ts`, #97): op-aware collapse of same-physical-key
298
+ collisions, the ≤25 enforcement, the atomic commit, and the CDC write-capture loop
299
+ all live in that one place, so no caller carries its own collapse / limit / commit
300
+ / capture logic.
301
+
302
+ Naming: the internal composition DSL (`src/define/mutation.ts`) is **not public**
303
+ the public Command IF is `graphddb.publishCommand({ alias: descriptor })`, where
304
+ each `descriptor` is `{ create | update | remove: Model, key, input?, … }`.
295
305
  `CommandPlan` = internal execution plan; `entityWrites` = model save-semantics.
296
- Fragment intents are the descriptor keys `create` / `update` / `remove`
297
- (`remove`, not `del`).
306
+ Descriptor intents are the keys `create` / `update` / `remove` (`remove`, not
307
+ `del`).
298
308
 
299
309
  This is the **same SSoT** as #64 (`CommandSpec`/`TransactionSpec`) and #70
300
310
  (`executionPlan`) — the compiler *populates* it from the adopted save contract
@@ -1,9 +1,9 @@
1
1
  # GraphDDB Prepared Statements
2
2
 
3
- `DDBModel.prepare($ => ({...}))` → `.execute(params)` is the read/write-unified
3
+ `graphddb.prepare($ => ({...}))` → `.execute(params)` is the read/write-unified
4
4
  **prepared statement**: a declarative route body is compiled **once**, and each
5
5
  `execute(params)` binds the per-call values into the precompiled plan and runs
6
- it through the **same execution cores** `DDBModel.mutate` / `Model.query` /
6
+ it through the **same execution cores** `graphddb.mutate` / `Model.query` /
7
7
  `Model.list` and the public CQRS contracts use — effects are identical by
8
8
  construction (design #203, runtime #205, compile-time transform #206, static
9
9
  AOT plans #208).
@@ -12,21 +12,21 @@ AOT plans #208).
12
12
 
13
13
  | Use | API |
14
14
  |---|---|
15
- | ad-hoc one-shot | `DDBModel.mutate({...})` / `Model.query(...)` / `Model.list(...)` — recompiles per call |
16
- | repeated hot path (prepared statement) | `DDBModel.prepare($ => ({...}))` → `.execute(params)` |
17
- | public CQRS contract | `publicCommandModel({...})` / `publicQueryModel({...})` |
15
+ | ad-hoc one-shot | `graphddb.mutate({...})` / `Model.query(...)` / `Model.list(...)` — recompiles per call |
16
+ | repeated hot path (prepared statement) | `graphddb.prepare($ => ({...}))` → `.execute(params)` |
17
+ | public CQRS contract | `graphddb.publishCommand({...})` / `graphddb.publishQuery({...})` |
18
18
 
19
19
  `prepare` is the missing middle: **precompiled without the contract ceremony**.
20
20
 
21
21
  ```ts
22
22
  // write
23
- const createPost = DDBModel.prepare(($) => ({
23
+ const createPost = graphddb.prepare(($) => ({
24
24
  post: { create: () => Post, key: { threadId: $.threadId, postId: $.postId }, input: { body: $.body } },
25
25
  }));
26
26
  await createPost.execute({ threadId, postId, body });
27
27
 
28
28
  // read — symmetric; only the key values (+ limit/cursor/consistentRead) are dynamic
29
- const userById = DDBModel.prepare(($) => ({
29
+ const userById = graphddb.prepare(($) => ({
30
30
  user: { query: () => User, key: { userId: $.userId }, select: { userId: true, name: true } },
31
31
  }));
32
32
  await userById.execute({ userId });
@@ -65,7 +65,7 @@ whole contract:
65
65
  | **AOT** (`graphddb transform prepared --aot <artifact> --write`, #208) | **zero compilation at runtime, including the first call** — the plan was compiled at BUILD time into a static artifact; the call site loads it and every call binds params into the frozen plan | identical |
66
66
  | **transform applied** (`graphddb transform prepared --write`) | zero per-op after the FIRST call (the lazy slot compiles once per module at first use) | identical |
67
67
  | **no transform** (fallback) | phase-1 **structural memoization**: the body is re-evaluated and structure-hashed per call, then the compiled handle is reused from a bounded LRU (no recompile) | identical |
68
- | hand-hoisted module-level `const stmt = DDBModel.prepare(...)` | compile once at first use (runtime); the transform leaves it untouched | identical |
68
+ | hand-hoisted module-level `const stmt = graphddb.prepare(...)` | compile once at first use (runtime); the transform leaves it untouched | identical |
69
69
 
70
70
  The transform exists to remove the placement footgun: without it, an inline
71
71
  `prepare` is *correct but warmer* than a module-level one (silent-slow). With
@@ -108,7 +108,7 @@ evaluates it:
108
108
 
109
109
  ```ts
110
110
  // after `--aot src/graphddb.prepared.ts --write`
111
- import { loadPreparedPlan as __gddbLoadPreparedPlan } from 'graphddb';
111
+ import { loadPreparedPlan as __gddbLoadPreparedPlan } from 'graphddb/internal';
112
112
  import __gddbPreparedPlans from './graphddb.prepared.js';
113
113
  let __gddbPrepared1;
114
114
  export async function getUser(userId: string) {
@@ -152,7 +152,7 @@ The rewrite normalizes each inline call site to a module-scope **lazy slot**:
152
152
  ```ts
153
153
  // before (inline, per-call structural memoization)
154
154
  export async function getUser(userId: string) {
155
- return DDBModel.prepare(($) => ({
155
+ return graphddb.prepare(($) => ({
156
156
  u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
157
157
  })).execute({ userId });
158
158
  }
@@ -160,7 +160,7 @@ export async function getUser(userId: string) {
160
160
  // after `graphddb transform prepared --write`
161
161
  let __gddbPrepared1;
162
162
  export async function getUser(userId: string) {
163
- return (__gddbPrepared1 ??= DDBModel.prepare(($) => ({
163
+ return (__gddbPrepared1 ??= graphddb.prepare(($) => ({
164
164
  u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
165
165
  }))).execute({ userId });
166
166
  }
@@ -172,7 +172,7 @@ file), the plan compiles once per module at first use, and every later call is
172
172
  a single nullish check straight into `.execute`. Files containing any
173
173
  violation are **never rewritten** — the build fails loudly instead.
174
174
 
175
- A file is scanned when it contains a `DDBModel.prepare` call site; detection
175
+ A file is scanned when it contains a `graphddb.prepare` call site; detection
176
176
  follows import aliases (`import { DDBModel as M }`), namespace imports
177
177
  (`g.DDBModel`), module-const aliases, and same-file `extends DDBModel`
178
178
  subclasses. Call sites already evaluated once per module load (module-scope
@@ -212,7 +212,7 @@ site is never wrong, only warmer.
212
212
 
213
213
  Detection also does **not check the import's module specifier**: a binding
214
214
  imported as `DDBModel` from **any** package (`import { DDBModel } from
215
- '<other-package>'`) matches, so another library's same-named `DDBModel.prepare`
215
+ '<other-package>'`) matches, so another library's same-named `graphddb.prepare`
216
216
  call site is transformed / linted too (a false transform or a spurious lint
217
217
  error is possible). Take care in files that mix graphddb with a same-named
218
218
  foreign API.
@@ -221,7 +221,7 @@ foreign API.
221
221
 
222
222
  - **write** — the params-independent op template compiles once
223
223
  (`compileWriteFragment`); `execute` runs the identical core the envelope and
224
- `publicCommandModel` use: derived maintainers and atomic
224
+ `graphddb.publishCommand` use: derived maintainers and atomic
225
225
  `TransactWriteItems` semantics match exactly. Multi-alias bodies compose one
226
226
  atomic transaction (`mode: 'parallel'` opts out per call).
227
227
  - **read** — the params-independent products (resolved model, normalized