graphddb 0.7.9 → 0.8.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.
Files changed (45) hide show
  1. package/README.md +6 -6
  2. package/dist/cdc/index.d.ts +389 -4
  3. package/dist/cdc/index.js +4 -3
  4. package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
  5. package/dist/chunk-HNY2EJPV.js +1184 -0
  6. package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
  7. package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
  8. package/dist/chunk-L4QRCHRQ.js +278 -0
  9. package/dist/chunk-LGHSZIEE.js +187 -0
  10. package/dist/chunk-N4NWYNGZ.js +1987 -0
  11. package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
  12. package/dist/cli.js +63 -252
  13. package/dist/index.d.ts +23 -1548
  14. package/dist/index.js +100 -1778
  15. package/dist/internal/index.d.ts +84 -0
  16. package/dist/internal/index.js +701 -0
  17. package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
  18. package/dist/linter/index.d.ts +39 -6
  19. package/dist/linter/index.js +22 -4
  20. package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
  21. package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
  22. package/dist/spec/index.d.ts +506 -4
  23. package/dist/spec/index.js +36 -17
  24. package/dist/testing/index.d.ts +2 -2
  25. package/dist/testing/index.js +4 -3
  26. package/dist/transform/index.d.ts +460 -1
  27. package/dist/transform/index.js +2085 -2
  28. package/dist/types-2PMXEn5x.d.ts +1205 -0
  29. package/dist/types-BXLzIcQD.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 +28 -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 +113 -66
  39. package/docs/spec.md +153 -124
  40. package/docs/testing.md +9 -8
  41. package/package.json +20 -5
  42. package/dist/chunk-MMVHOUM4.js +0 -24
  43. package/dist/from-change-DanwjE5b.d.ts +0 -327
  44. package/dist/index-CtPJSMrc.d.ts +0 -934
  45. package/dist/relation-depth-Dg3yhl7S.d.ts +0 -36
@@ -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
@@ -33,7 +33,7 @@ access patterns that TypeScript has already defined and validated.
33
33
  ```text
34
34
  TypeScript (design-time / build-time SSoT)
35
35
  ├─ Entity / Key / GSI / Relation models
36
- ├─ Parameterized query / command definitions (param + define*)
36
+ ├─ Parameterized query / command contracts (param + graphddb.publish*)
37
37
  ├─ Static parameterized planner (symbolic key evaluation)
38
38
  ├─ Bridge guard (reject non-declarative / non-serializable specs)
39
39
  └─ Bridge bundle: manifest.json + operations.json
@@ -51,7 +51,7 @@ boto3 DynamoDB client
51
51
  | Layer | Responsibility |
52
52
  |---|---|
53
53
  | TypeScript model | Entity / key / GSI / relation definitions |
54
- | Definition DSL (`param`, `define*`) | The permitted, parameterized queries and commands |
54
+ | Contract DSL (`param`, `graphddb.publish*`) | The permitted, parameterized queries and commands |
55
55
  | Static planner (`src/spec/*`) | Symbolic key evaluation → `manifest.json` / `operations.json` |
56
56
  | Generator (`src/codegen/python.ts`, `src/cli/*`) | Python repositories, result types, package exports |
57
57
  | Python repository | Stable application-facing interface |
@@ -69,68 +69,96 @@ runtime descriptor (`kind`, optional `literals`, optional element shape) that
69
69
  the planner and generator read. `param.literal('active', 'disabled')` has value
70
70
  type `'active' | 'disabled'`; the literal set is preserved in the spec.
71
71
 
72
- Parameters are legal only inside the structures passed to the `define*` entry
73
- points — never to the live `Model.query` / `Model.putItem` runtime API, whose types
74
- remain free of parameter placeholders.
72
+ Parameters are legal only inside the structures passed to the contract authoring
73
+ entry points (`graphddb.publishQuery` / `graphddb.publishCommand`) never to the
74
+ live `Model.query` / `Model.putItem` runtime API, whose types remain free of
75
+ parameter placeholders.
75
76
 
76
- ### 3.2 Definition Entry Points
77
+ ### 3.2 Contract Authoring Entry Points
77
78
 
78
- Definitions are built with dedicated entry points, each accepting `param`
79
+ Public reads and writes are authored through the CQRS contract layer, the sole
80
+ read/write authoring surface. Import the `graphddb` namespace and publish keyed
81
+ contracts whose methods are declarative descriptors, each accepting `param`
79
82
  placeholders at scalar leaves while checking field names, value types, the key
80
83
  boundary, and the strict select against the model:
81
84
 
82
- - `defineQuery(Model, key, select)` — single-item read (unique key).
83
- - `defineList(Model, key, select)` partition read (partial / partition key).
84
- - `definePut(Model, item, options?)` — put.
85
- - `defineUpdate(Model, key, changes, options?)` — update.
86
- - `defineDelete(Model, key, options?)` — delete.
85
+ ```ts
86
+ import { graphddb, param } from 'graphddb';
87
+ ```
88
+
89
+ - `graphddb.publishQuery({ name: { get: { query: Model, key, select } } })` —
90
+ single-item read (unique key).
91
+ - `graphddb.publishQuery({ name: { list: { list: Model, key, select } } })` —
92
+ partition read (partial / partition key).
93
+ - `graphddb.publishCommand({ name: { create: Model, key, input } })` — put.
94
+ - `graphddb.publishCommand({ name: { update: Model, key, input } })` — update.
95
+ - `graphddb.publishCommand({ name: { remove: Model, key } })` — delete.
87
96
 
88
- Write entry points accept an optional `{ condition }` (see §6.2). Each entry
89
- point collects its placeholders into a `name ParamDescriptor` map; a parameter
90
- name used twice with conflicting types is rejected at definition time.
97
+ A command method descriptor accepts an optional `condition` (see §6.2) and an
98
+ optional `result: { select }` read-back projection. Each published contract
99
+ collects its placeholders into a `name ParamDescriptor` map; a parameter name
100
+ used twice with conflicting types is rejected at definition time.
91
101
 
92
- > Design note: the entry points are dedicated functions (not `Model.query(...)`
93
- > overloads) so that the live runtime model API is never widened to accept
94
- > parameter placeholders.
102
+ > Design note: the authoring surface is `graphddb.publishQuery` /
103
+ > `graphddb.publishCommand` (not `Model.query(...)` overloads), so the live
104
+ > runtime model API is never widened to accept parameter placeholders.
95
105
 
96
106
  ### 3.3 Grouping
97
107
 
98
- `defineQueries({ ... })` groups read definitions (`defineQuery` / `defineList`);
99
- `defineCommands({ ... })` groups write definitions (`definePut` / `defineUpdate`
100
- / `defineDelete`). Each validates that every entry is the correct operation kind
101
- and returns the typed record that the planner consumes. `defineTransactions({
102
- ... })` groups declarative transactions (see §6.3).
108
+ `graphddb.publishQuery({ ... })` publishes a keyed read contract whose methods
109
+ resolve to `get` / `list` descriptors; `graphddb.publishCommand({ ... })`
110
+ publishes a keyed write contract whose methods resolve to `create` / `update` /
111
+ `remove` descriptors. Each validates that every method is the correct operation
112
+ kind and returns the typed contract that the planner consumes. Atomic multi-op
113
+ writes are declared with a `mode: 'transaction'` descriptor and compile to a
114
+ declarative `TransactionSpec` (see §6.3).
103
115
 
104
116
  ```ts
105
- export const queries = defineQueries({
106
- getUserByEmail: defineQuery(
107
- User,
108
- { email: param.string() },
109
- { userId: true, name: true, email: true, status: true },
110
- ),
111
- listGroupMembers: defineList(
112
- GroupMembership,
113
- { groupId: param.string() },
114
- { userId: true, role: true, joinedAt: true },
115
- ),
117
+ import { graphddb, param } from 'graphddb';
118
+
119
+ export const UserByEmail = graphddb.publishQuery({
120
+ get: {
121
+ query: User,
122
+ key: { email: param.string() },
123
+ select: { userId: true, name: true, email: true, status: true },
124
+ },
125
+ });
126
+
127
+ export const MembersByGroup = graphddb.publishQuery({
128
+ list: {
129
+ list: GroupMembership,
130
+ key: { groupId: param.string() },
131
+ select: { userId: true, role: true, joinedAt: true },
132
+ },
116
133
  });
117
134
 
118
- export const commands = defineCommands({
119
- addGroupMember: definePut(GroupMembership, {
120
- groupId: param.string(),
121
- userId: param.string(),
122
- role: param.string(),
123
- }),
124
- disableUser: defineUpdate(
125
- User,
126
- { userId: param.string() },
127
- { status: param.literal('disabled') },
128
- ),
129
- deleteGroupMember: defineDelete(GroupMembership, {
130
- groupId: param.string(),
131
- userId: param.string(),
132
- }),
135
+ export const MembershipCommands = graphddb.publishCommand({
136
+ addGroupMember: {
137
+ create: GroupMembership,
138
+ key: { groupId: param.string(), userId: param.string() },
139
+ input: { role: param.string() },
140
+ },
141
+ deleteGroupMember: {
142
+ remove: GroupMembership,
143
+ key: { groupId: param.string(), userId: param.string() },
144
+ },
133
145
  });
146
+
147
+ export const UserCommands = graphddb.publishCommand({
148
+ disableUser: {
149
+ update: User,
150
+ key: { userId: param.string() },
151
+ input: { status: param.literal('disabled') },
152
+ },
153
+ });
154
+
155
+ // The `contracts` map the generator consumes (see §5).
156
+ export const contracts = {
157
+ UserByEmail,
158
+ MembersByGroup,
159
+ MembershipCommands,
160
+ UserCommands,
161
+ };
134
162
  ```
135
163
 
136
164
  ## 4. The Bridge Bundle
@@ -209,7 +237,7 @@ so the output is stable across runs.
209
237
 
210
238
  ```ts
211
239
  interface OperationsDocument {
212
- readonly version: '1.1';
240
+ readonly version: '1.1' | '1.2';
213
241
  readonly queries: Record<string, QuerySpec>;
214
242
  readonly commands: Record<string, CommandSpec>;
215
243
  readonly transactions?: Record<string, TransactionSpec>;
@@ -218,6 +246,22 @@ interface OperationsDocument {
218
246
  }
219
247
  ```
220
248
 
249
+ **Spec version `1.2` — SCP Expression IR (issue #261, conditional stamping).**
250
+ `1.2` adds the behavior-contracts Expression IR vocabulary
251
+ (`expression-ir.md` §2) to the operations document: a sixth write-condition
252
+ variant `{ kind: 'scpExpr', expression: { exprVersion: 1, expr } }`, and a
253
+ `guard: { exprVersion: 1, expr }` slot on transaction items and contract
254
+ command methods (beside the legacy `when`). The expression payload is stored in
255
+ **canonical form** (key-sorted in code-point order; `{int:"…"}` /
256
+ `{float:n}` literal wrapping per expression-ir.md §2.3). The stamp is
257
+ **conditional**: only a document that actually *contains* an SCP node is
258
+ stamped `1.2` — every SCP-free document keeps `1.1` and serializes
259
+ byte-identically. Runtimes keep `SPEC_VERSION_SUPPORTED = "1.1"` until their
260
+ expression *evaluation* lands (Phase 3 S5/S6/S7 — #265/#266/#267), so an
261
+ expression-bearing document is loud-rejected everywhere in the meantime
262
+ (fail-closed, never a silently-skipped guard). The manifest never carries
263
+ expression vocabulary and always stays `1.1`.
264
+
221
265
  #### Parameter specs
222
266
 
223
267
  Every operation carries a `params` map of `ParamSpec`:
@@ -314,20 +358,17 @@ GSI-keyed write is rejected by the planner.
314
358
  ```bash
315
359
  graphddb generate python \
316
360
  --entry src/models/index.ts \
317
- --queries src/models/queries.ts \
318
- --commands src/models/commands.ts \
361
+ --contracts src/models/contracts.ts \
319
362
  --out generated/graphddb
320
363
  ```
321
364
 
322
365
  | Flag | Required | Default | Meaning |
323
366
  |---|---|---|---|
324
- | `--entry`, `-e` | yes | — | Module that registers entity models (imported for side effects); may also export the definition maps. |
367
+ | `--entry`, `-e` | yes | — | Module that registers entity models (imported for side effects); may also export `contracts` / `contexts` / `transactions`. |
325
368
  | `--out`, `-o` | yes | — | Output directory (created if absent). |
326
- | `--queries`, `-q` | no | `--entry` | Module exporting `queries`. |
327
- | `--commands`, `-c` | no | `--entry` | Module exporting `commands`. |
328
- | `--transactions`, `-t` | no | `--commands` then `--entry` | Module exporting `transactions`. |
329
- | `--contracts` | no | `--entry` | Module exporting CQRS `contracts`. |
369
+ | `--contracts` | no | `--entry` | Module exporting the CQRS `contracts` map (`graphddb.publishQuery` / `graphddb.publishCommand` results). |
330
370
  | `--contexts` | no | `--contracts` then `--entry` | Module exporting context ownership. |
371
+ | `--transactions`, `-t` | no | `--entry` | Module exporting `transactions`. |
331
372
  | `--dataclass` | no | `false` | Emit `@dataclass` result types instead of `TypedDict`. |
332
373
 
333
374
  TypeScript definition modules are transpiled on the fly (via tsx/esbuild — the
@@ -439,15 +480,21 @@ the serialized `expr` tree). (Per-item conditions are not available on
439
480
 
440
481
  ### 6.3 Transactions (declarative subset)
441
482
 
442
- `defineTransaction` records a declarative transaction whose body may only call
443
- `tx.put` / `tx.update` / `tx.delete` with field references or literals at scalar
444
- leaves, iterate an array parameter with `tx.forEach(p.<arrayParam>, ...)`, and
445
- attach a declarative `when` comparison (`eq` / `ne`) and/or a supported
446
- `condition`. The planner emits a `TransactionSpec` whose `items` carry rendered
447
- templates (`{param}`, `{item.field}` for `forEach` elements), optional
448
- `condition`, optional `when`, and optional `forEach: { source }`. A transaction
449
- with no `forEach` is rejected at build time if it exceeds 25 items; with
450
- `forEach`, the 25-item limit is enforced by the runtime after expansion.
483
+ An atomic multi-op write — a `mode: 'transaction'` command descriptor, or the
484
+ derived effects of any command (edges, counters, uniqueness guards, outbox,
485
+ idempotency, maintenance writes) compiles to a declarative `TransactionSpec`.
486
+ Its `items` write with field references or literals at scalar leaves; a bulk
487
+ `key: K[]` command lowers to a single `forEach`-over-keys item; and each item may
488
+ carry a declarative `when` comparison (`eq` / `ne`) and/or a supported
489
+ `condition`. The `TransactionSpec` `items` carry rendered templates (`{param}`,
490
+ `{item.field}` for `forEach` elements), optional `condition`, optional `when`, and
491
+ optional `forEach: { source }`. A transaction with no `forEach` is rejected at
492
+ build time if it exceeds 25 items; with `forEach`, the 25-item limit is enforced
493
+ by the runtime after expansion.
494
+
495
+ (The internal declarative-transaction recorder that produces this IR is the
496
+ `publishCommand` / `mutate` compile backend and the `graphddb transform`
497
+ lowering target — it is not a public authoring verb.)
451
498
 
452
499
  ## 7. The Python Runtime (`graphddb_runtime`)
453
500