graphddb 0.8.1 → 0.9.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.
@@ -0,0 +1,217 @@
1
+ import { Recorded, Component, ComponentGraphIR, PortSchema, BehaviorClass } from 'behavior-contracts';
2
+
3
+ /**
4
+ * GraphDDB's first-class SCP catalog — issue #297 SP1 (SCP authoring adoption),
5
+ * promoting the Phase-1 executor catalog (#288, `src/executor/catalog.ts`) into
6
+ * THE behavior-contracts–typed catalog the spec emitter is validated against.
7
+ *
8
+ * ## Source of truth (SP1 decision)
9
+ *
10
+ * ONE catalog lives here, in the spec layer — the layer that emits `components[]`
11
+ * (`src/spec/components.ts`) — and `src/executor/catalog.ts` re-exports it
12
+ * unchanged (the C4 handler registry keeps resolving against the same object).
13
+ * The Phase-1 module described itself as the C2 "the only per-DSL difference is
14
+ * the catalog" surface but its Port schemas mirrored the OLD-path executor
15
+ * inputs (`DynamoDBOperation` / `PutInput`), written before the Phase-3 emitter
16
+ * existed. Since #290 P3-5 the portable IR's actual wiring is the emitted
17
+ * component-graph ports, so promoting the catalog to LOAD-BEARING means the
18
+ * schemas here describe the EMITTED Port contract (what `readPorts` /
19
+ * `transactionItemPorts` in `components.ts` actually wire, and what a
20
+ * `runBehavior` handler actually receives) — the two latent Phase-1↔Phase-3
21
+ * inconsistencies are fixed loudly, not papered over:
22
+ *
23
+ * 1. **`ConditionCheck` was missing.** The emitter has emitted `ConditionCheck`
24
+ * componentRefs since Phase 3 (`buildTransactionComponent`), but the Phase-1
25
+ * catalog declared only 7 entries without it — so the "catalog" did not cover
26
+ * the emitted vocabulary. It is now a first-class entry.
27
+ * 2. **`TransactWrite` is not portable.** No portable document references
28
+ * `TransactWrite` (a transaction is emitted as a Component whose body
29
+ * references the per-item write primitives; the atomic TransactWriteItems
30
+ * envelope is the TS runtime's execution concern) — so its Phase-1
31
+ * `portableToIR: true` claim was false since P3-5. It stays a catalog entry
32
+ * (the executor-side atomic envelope, and a member of the write set) but is
33
+ * now honestly `portableToIR: false`.
34
+ *
35
+ * ## Port schema conventions
36
+ *
37
+ * A `CatalogEntry.inputPorts` names the STATIC ports of the emitted wiring. Two
38
+ * port families are dynamic by construction and are validated by
39
+ * {@link assertComponentsInCatalog} against documented patterns instead:
40
+ *
41
+ * - **Read key-attribute ports**: one port per `keyExpr` attribute, named by the
42
+ * physical key attribute — graphddb's fixed single-table convention
43
+ * `PK` / `SK` / `<index>PK` / `<index>SK` ({@link KEY_ATTR_PORT}).
44
+ * - **Write record ports**: a transaction item's `item` / `key` / `changes` /
45
+ * `add` records and the `condition.equals` / `condition.params` /
46
+ * `condition.values` slots are flattened one-port-per-field under a
47
+ * `<group>.<field>` key ({@link WRITE_PORT_FAMILIES}).
48
+ *
49
+ * ## Dynamic-port opt-in (`additionalPorts`, SP3)
50
+ *
51
+ * The seven read/write-item entries carry `additionalPorts: true` (bc#26): their
52
+ * dynamic port families above are BY DESIGN not statically enumerable, so bc's
53
+ * shared port check (`checkPortNames` — used by both the `catalogComponents`
54
+ * authoring recorder and `buildComponentDefinition`) must not reject them. This
55
+ * is an honest declaration, not a loosening: the dynamic families are still
56
+ * validated fail-closed by {@link assertComponentsInCatalog} (emit/registration
57
+ * time), and every DECLARED port keeps bc's required/type checks regardless of
58
+ * the opt-in. It is exactly this opt-in that makes `GRAPHDDB_CATALOG` directly
59
+ * consumable by the shared authoring surface (`catalogComponents(GRAPHDDB_CATALOG)`,
60
+ * bc#24 / #297 SP3) — the C2 contract that the catalog IS the whole per-DSL
61
+ * difference. `TransactWrite` stays fail-closed (its one port is static).
62
+ *
63
+ * ## Write set (SP1b) — and where `ConditionCheck` sits
64
+ *
65
+ * {@link WRITE_CATALOG_NAMES} is the CQRS write catalog: `PutItem` /
66
+ * `UpdateItem` / `DeleteItem` / `TransactWrite` / `ConditionCheck`.
67
+ * `ConditionCheck` mutates nothing — but it is classified WRITE deliberately:
68
+ * it exists only inside `TransactWriteItems` (DynamoDB accepts it in no read
69
+ * API), it acquires the item's transactional lock, and its failure aborts the
70
+ * sibling writes. A component referencing it therefore can only ever execute on
71
+ * the command path — for effect ROUTING (which is all the write set decides,
72
+ * bc#24) it is command-side infrastructure, not a read. The read set is exactly
73
+ * `GetItem` / `Query` / `BatchGetItem`; every catalog name is in exactly one set.
74
+ */
75
+
76
+ /**
77
+ * The catalog-name literal union — one entry per DynamoDB primitive GraphDDB
78
+ * declares: the `ReadOperationType` (`GetItem` / `Query` / `BatchGetItem`) and
79
+ * `WriteOperationType` (`PutItem` / `UpdateItem` / `DeleteItem`) enum members
80
+ * from `src/spec/types.ts`, the transaction-item assertion primitive
81
+ * (`ConditionCheck`), and the atomic envelope (`TransactWrite`).
82
+ */
83
+ type CatalogName = 'GetItem' | 'Query' | 'BatchGetItem' | 'PutItem' | 'UpdateItem' | 'DeleteItem' | 'ConditionCheck' | 'TransactWrite';
84
+ /** GraphDDB's CQRS effect vocabulary, mapped 1:1 from bc's `BehaviorEffect`. */
85
+ type ContractEffect = 'query' | 'command';
86
+
87
+ /**
88
+ * `publishBehaviors` — the shared-SCP authoring entry (issue #297 SP3; pairs
89
+ * graphddb with behavior-contracts bc#24 and connects to #296 P4-1).
90
+ *
91
+ * A user authors a **`SemanticBehavior` class** on behavior-contracts' SHARED
92
+ * authoring surface — `$` input wiring, `when` / `.map` structured control, the
93
+ * expression builders (`eq` / `concat` / `coalesce` / …), and the leaf
94
+ * components minted from graphddb's catalog via
95
+ * `catalogComponents(GRAPHDDB_CATALOG)` ({@link behaviorComponents}) — and
96
+ * publishes it here:
97
+ *
98
+ * ```ts
99
+ * import { graphddb, behaviorComponents } from 'graphddb';
100
+ * import { SemanticBehavior, concat, type In } from 'behavior-contracts';
101
+ *
102
+ * const G = behaviorComponents();
103
+ * class ArticleBehaviors extends SemanticBehavior {
104
+ * getArticleWithTags($: In<{ articleId: string }>) {
105
+ * const a = G.GetItem({ table: 'Articles', PK: concat('ARTICLE#', $.articleId), SK: 'META',
106
+ * projection: ['articleId', 'title', 'tagRefs'] });
107
+ * const tags = a.tagRefs.map(($t: Recorded) =>
108
+ * G.BatchGetItem({ table: 'Articles', PK: concat('TAG#', $t.tagId), SK: 'META',
109
+ * projection: ['tagId', 'label'] }), { batched: true });
110
+ * return { article: a, tags };
111
+ * }
112
+ * }
113
+ * const Articles = graphddb.publishBehaviors(ArticleBehaviors);
114
+ * ```
115
+ *
116
+ * ## Responsibility split (C2 — "the difference is the catalog only")
117
+ *
118
+ * - **behavior-contracts does ALL composition and lowering**: `compileBehaviors`
119
+ * evaluates each public method once (throwing-sentinel recording + native
120
+ * control-syntax source scan), lowers `$` references, component calls, `when`
121
+ * and `.map` into the portable component-graph IR, derives the plan, and
122
+ * self-checks the result with `assertPortableComponentGraph`. graphddb adds
123
+ * **no generic lowering of its own**.
124
+ * - **graphddb supplies only the catalog and the effects**: the leaf vocabulary
125
+ * is `GRAPHDDB_CATALOG` (#297 SP1 — the same load-bearing catalog the spec
126
+ * emitter is validated against), the dynamic port families are validated by
127
+ * the same {@link assertComponentsInCatalog}, and the Query/Command
128
+ * classification is DERIVED from the lowered graph per SP2/SP4
129
+ * ({@link deriveContractEffect}) — the author never declares an effect
130
+ * (authoring is effect-agnostic; a method that references a write-catalog
131
+ * primitive IS a command).
132
+ *
133
+ * The compiled components register additively into the operations document
134
+ * through `buildOperations`' `contractInputs.behaviors` (each component emitted
135
+ * as `<RegistrationKey>__<method>`), and the TS runtime executes them through
136
+ * bc's `runBehavior` with graphddb's real DynamoDB handlers
137
+ * (`src/executor/behavior-exec.ts` — the Phase-1 handler-registry seam's
138
+ * behavior-execution counterpart, mirroring `python/graphddb_runtime/behavior_exec.py`).
139
+ */
140
+
141
+ /**
142
+ * One published behavior method: the bc-lowered component (name = the method
143
+ * name), plus its graph-DERIVED CQRS effect (#297 SP2/SP4 — the classification
144
+ * source of truth is the component graph; `publishBehaviors` has no
145
+ * query/command vocabulary for the author to declare).
146
+ */
147
+ interface BehaviorMethodSpec {
148
+ /** The method (= root Behavior = component) name. */
149
+ readonly name: string;
150
+ /** Graph-derived Query/Command classification (never author-declared). */
151
+ readonly effect: ContractEffect;
152
+ /** The lowered portable component (bc `compileBehaviors` output). */
153
+ readonly component: Component;
154
+ }
155
+ /**
156
+ * A resolved behavior contract — `publishBehaviors` output. Carries the whole
157
+ * bc-lowered {@link ComponentGraphIR} (directly `runBehavior`-executable; the
158
+ * component names are the method names) and the per-method specs with their
159
+ * derived effects. Register it into the operations document via
160
+ * `buildOperations(..., { behaviors: { Name: contract } })`.
161
+ */
162
+ interface BehaviorModelContract {
163
+ /** Discriminates from the publishQuery/publishCommand contract objects. */
164
+ readonly kind: 'behaviors';
165
+ /** The authored class name (diagnostics only). */
166
+ readonly className: string;
167
+ /** The bc-lowered portable IR (all methods; `runBehavior`-executable). */
168
+ readonly ir: ComponentGraphIR;
169
+ /** The lowered components (`ir.components`, one per public method). */
170
+ readonly components: readonly Component[];
171
+ /** Method name → lowered component + derived effect. */
172
+ readonly methods: Readonly<Record<string, BehaviorMethodSpec>>;
173
+ }
174
+ /** Options passed through to bc `compileBehaviors` (additive; all optional). */
175
+ interface PublishBehaviorsOptions {
176
+ /**
177
+ * Input Port schema overrides (method → port → schema): build-time evaluation
178
+ * derives port NAMES from `$` accesses; TS types are erased, so supply the
179
+ * type strings here if the registered document should carry them.
180
+ */
181
+ readonly inputPorts?: Record<string, Record<string, PortSchema>>;
182
+ /** Plan concurrency recorded on the lowered components (bc default: 16). */
183
+ readonly concurrency?: number;
184
+ }
185
+ /** The `catalogComponents(GRAPHDDB_CATALOG)` leaf-function map (typed). */
186
+ type BehaviorComponentFns = Record<CatalogName, (ports: Record<string, unknown>) => Recorded>;
187
+ /**
188
+ * The graphddb leaf components for the shared authoring surface:
189
+ * `catalogComponents(GRAPHDDB_CATALOG)` bound once, lazily. Call it at module
190
+ * scope of an authoring module (`const G = behaviorComponents()`) and invoke
191
+ * the leaves inside `SemanticBehavior` method bodies. `TransactWrite` is in the
192
+ * map but is `portableToIR: false`, so wiring it is rejected at record time —
193
+ * the atomic envelope is a TS-runtime execution concern, never authored.
194
+ */
195
+ declare function behaviorComponents(): BehaviorComponentFns;
196
+ /**
197
+ * Publish a `SemanticBehavior` class as a graphddb behavior contract (#297 SP3).
198
+ *
199
+ * Pipeline (all loud, nothing dropped):
200
+ * 1. bc `compileBehaviors(cls)` lowers every public method into one portable
201
+ * component (bc owns ALL composition/lowering and self-checks portability);
202
+ * 2. graphddb validates every lowered component against `GRAPHDDB_CATALOG` —
203
+ * the SAME {@link assertComponentsInCatalog} the spec emitter runs (SP1c),
204
+ * covering the dynamic key-attribute / write record-family ports bc's
205
+ * static schema check cannot enumerate;
206
+ * 3. the CQRS effect of every method is DERIVED from its graph
207
+ * ({@link deriveContractEffect} — SP2/SP4; the author never declares it).
208
+ *
209
+ * @throws bc `AuthoringFailure` for a non-declarative body (native control
210
+ * syntax, coercion, unknown/missing ports, …); an `Error` from the catalog
211
+ * validation for an undeclared dynamic port.
212
+ */
213
+ declare function publishBehaviors(cls: BehaviorClass, options?: PublishBehaviorsOptions): BehaviorModelContract;
214
+ /** Runtime guard: is `value` a {@link BehaviorModelContract}? */
215
+ declare function isBehaviorModelContract(value: unknown): value is BehaviorModelContract;
216
+
217
+ export { type BehaviorModelContract as B, type PublishBehaviorsOptions as P, type BehaviorMethodSpec as a, type BehaviorComponentFns as b, behaviorComponents as c, isBehaviorModelContract as i, publishBehaviors as p };
@@ -1,7 +1,8 @@
1
- import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-DZtjAQDh.js';
2
- export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-DZtjAQDh.js';
3
- import { E as EntityMetadata } from '../types-DW__-Icc.js';
4
- import '../types-2PMXEn5x.js';
1
+ import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-CWytoEaE.js';
2
+ export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-CWytoEaE.js';
3
+ import { E as EntityMetadata } from '../types-nk5okD7d.js';
4
+ import '../types-CgXS-4Ox.js';
5
+ import 'behavior-contracts';
5
6
 
6
7
  /**
7
8
  * CDC emulator (issue #72). Subscribes to the core write-capture seam
package/dist/cdc/index.js CHANGED
@@ -8,12 +8,12 @@ import {
8
8
  createMaintenanceDrainHandler,
9
9
  hashString,
10
10
  shardIdFor
11
- } from "../chunk-GS4C5VGO.js";
11
+ } from "../chunk-VECUS35D.js";
12
12
  import {
13
13
  buildSubscribeHandler,
14
14
  parseChange
15
- } from "../chunk-HNY2EJPV.js";
16
- import "../chunk-L2NEDS7U.js";
15
+ } from "../chunk-7OCXY4R6.js";
16
+ import "../chunk-GWWRXIHF.js";
17
17
  import "../chunk-XTWXMOHD.js";
18
18
  export {
19
19
  CdcEmulator,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MetadataRegistry,
3
3
  attachHiddenKey
4
- } from "./chunk-L2NEDS7U.js";
4
+ } from "./chunk-GWWRXIHF.js";
5
5
  import {
6
6
  resolveKey,
7
7
  segmentFieldNames
@@ -227,10 +227,35 @@ var param = {
227
227
  const param2 = { kind: "array", element };
228
228
  if (options?.description !== void 0) param2.description = options.description;
229
229
  return param2;
230
+ },
231
+ /**
232
+ * A placeholder for a **map / embedded object** value written to a single
233
+ * DynamoDB `M` attribute (issue #295). Free-form by design: the model-side
234
+ * vocabulary (`@map` / `@embedded`) records no nested schema, so the param is
235
+ * typed as the language-native map in generated bindings (`dict` /
236
+ * `map[string]any` / `serde_json::Value` / `array`) while the optional type
237
+ * parameter `T` keeps the TS authoring surface precise.
238
+ *
239
+ * Legal at **value positions only** (a write descriptor's `input`, a
240
+ * transaction fragment value, a batch-write per-item payload field) — a map
241
+ * cannot compose a key, so a `param.map()` in a key position is rejected
242
+ * loudly at build time.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * putLeaf: {
247
+ * upsert: TreeNode,
248
+ * key: { part: param.string(), child: param.string() },
249
+ * input: { value: param.map<AvgValue>(), computedAt: param.number() },
250
+ * }
251
+ * ```
252
+ */
253
+ map(options) {
254
+ return makeParam("map", void 0, options?.description);
230
255
  }
231
256
  };
232
257
  function isParam(value) {
233
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
258
+ return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array" || value.kind === "map");
234
259
  }
235
260
 
236
261
  // src/client/lib-dynamodb.ts
@@ -337,17 +362,220 @@ async function executeBatchGet(docClient, operation) {
337
362
  return { items: allItems };
338
363
  }
339
364
 
365
+ // src/spec/catalog.ts
366
+ import { classifyBehaviorEffect } from "behavior-contracts";
367
+ var P = (type, required) => required ? { type, required } : { type };
368
+ var READ_PORTS = {
369
+ table: P("string", true),
370
+ indexName: P("string"),
371
+ rangeKey: P("string"),
372
+ rangeOperator: P("string"),
373
+ rangeValue: P("expr"),
374
+ limit: P("number"),
375
+ filter: P("string"),
376
+ projection: P("string[]", true),
377
+ implicitSourceField: P("string")
378
+ };
379
+ var WRITE_PORTS = {
380
+ table: P("string", true),
381
+ entity: P("string", true),
382
+ literalKey: P("boolean"),
383
+ maintain: P("string"),
384
+ "condition.kind": P("string"),
385
+ "condition.field": P("string"),
386
+ "condition.declarative": P("string"),
387
+ "condition.expression": P("string"),
388
+ "condition.names": P("string")
389
+ };
390
+ var entry = (name, inputPorts, shape, portableToIR) => ({
391
+ name,
392
+ inputPorts,
393
+ output: { shape },
394
+ portableToIR,
395
+ // Dynamic port families (key-attribute / write record ports) are validated by
396
+ // assertComponentsInCatalog, not statically enumerable — see the module doc
397
+ // ("Dynamic-port opt-in"). TransactWrite overrides this back to fail-closed.
398
+ additionalPorts: true
399
+ });
400
+ var GRAPHDDB_CATALOG = {
401
+ GetItem: entry("GetItem", READ_PORTS, "item", true),
402
+ Query: entry(
403
+ "Query",
404
+ // `cardinality: 'one'` is the root-collapse port only the Query root emits.
405
+ { ...READ_PORTS, cardinality: P("string") },
406
+ "items",
407
+ true
408
+ ),
409
+ BatchGetItem: entry("BatchGetItem", READ_PORTS, "items", true),
410
+ PutItem: entry("PutItem", WRITE_PORTS, "item", true),
411
+ UpdateItem: entry("UpdateItem", WRITE_PORTS, "item", true),
412
+ DeleteItem: entry("DeleteItem", WRITE_PORTS, "item", true),
413
+ ConditionCheck: entry("ConditionCheck", WRITE_PORTS, "item", true),
414
+ // The executor-side atomic envelope: never referenced by the portable IR
415
+ // (a transaction IS a component over the item primitives) — TS-runtime-only.
416
+ // Its single port is static, so it keeps bc's fail-closed default (no
417
+ // additionalPorts opt-in).
418
+ TransactWrite: {
419
+ ...entry("TransactWrite", { TransactItems: P("map[]", true) }, "item", false),
420
+ additionalPorts: false
421
+ }
422
+ };
423
+ function catalogEntry(name) {
424
+ return GRAPHDDB_CATALOG[name];
425
+ }
426
+ var CATALOG_NAMES = [
427
+ "GetItem",
428
+ "Query",
429
+ "BatchGetItem",
430
+ "PutItem",
431
+ "UpdateItem",
432
+ "DeleteItem",
433
+ "ConditionCheck",
434
+ "TransactWrite"
435
+ ];
436
+ var WRITE_CATALOG_NAMES = [
437
+ "PutItem",
438
+ "UpdateItem",
439
+ "DeleteItem",
440
+ "ConditionCheck",
441
+ "TransactWrite"
442
+ ];
443
+ var WRITE_SET = new Set(WRITE_CATALOG_NAMES);
444
+ var KEY_ATTR_PORT = /^[A-Za-z0-9_]*(?:PK|SK)$/;
445
+ var WRITE_PORT_FAMILIES = [
446
+ "item.",
447
+ "key.",
448
+ "changes.",
449
+ "add.",
450
+ "condition.equals.",
451
+ "condition.params.",
452
+ "condition.values."
453
+ ];
454
+ function assertComponentsInCatalog(components) {
455
+ const errs = [];
456
+ for (const c of components) {
457
+ for (const n of c.body) {
458
+ if ("cond" in n) continue;
459
+ const ref = "map" in n ? n.map : n;
460
+ const entryDef = GRAPHDDB_CATALOG[ref.component];
461
+ if (entryDef === void 0) {
462
+ errs.push(`${c.name}/${n.id}: references '${ref.component}', not a GRAPHDDB_CATALOG entry (known: ${CATALOG_NAMES.join(", ")})`);
463
+ continue;
464
+ }
465
+ const ports = ref.ports;
466
+ for (const [p, s] of Object.entries(entryDef.inputPorts)) {
467
+ if (s.required === true && !(p in ports)) {
468
+ errs.push(`${c.name}/${n.id} (${ref.component}): required port '${p}' is not wired`);
469
+ }
470
+ }
471
+ const isWriteItem = WRITE_SET.has(ref.component);
472
+ for (const p of Object.keys(ports)) {
473
+ if (p in entryDef.inputPorts) continue;
474
+ if (isWriteItem ? WRITE_PORT_FAMILIES.some((f) => p.startsWith(f)) : KEY_ATTR_PORT.test(p)) continue;
475
+ errs.push(`${c.name}/${n.id} (${ref.component}): port '${p}' is not declared by the catalog entry (nor a ${isWriteItem ? "write record-family" : "key-attribute"} port)`);
476
+ }
477
+ }
478
+ }
479
+ if (errs.length > 0) {
480
+ throw new Error(`graphddb catalog validation failed (${errs.length} violation${errs.length === 1 ? "" : "s"}):
481
+ - ${errs.join("\n - ")}`);
482
+ }
483
+ }
484
+ function deriveContractEffect(component) {
485
+ return classifyBehaviorEffect(component, WRITE_CATALOG_NAMES) === "writing" ? "command" : "query";
486
+ }
487
+ function deriveComponentEffects(components) {
488
+ const out = /* @__PURE__ */ new Map();
489
+ for (const c of components) out.set(c.name, deriveContractEffect(c));
490
+ return out;
491
+ }
492
+ function assertPublishSplitMatchesDerived(derived, split, contracts) {
493
+ const errs = [];
494
+ const check = (name, declared, at) => {
495
+ const truth = derived.get(name);
496
+ if (truth === void 0) {
497
+ errs.push(`${at}: no emitted component '${name}'`);
498
+ return;
499
+ }
500
+ if (truth !== declared) {
501
+ const verb = declared === "query" ? "publishQuery" : "publishCommand";
502
+ const fix = truth === "command" ? "publishCommand" : "publishQuery";
503
+ errs.push(
504
+ `${at}: published under ${verb} (declares '${declared}') but component '${name}' derives '${truth}' from its graph \u2014 the component graph is the source of truth; publish it with ${fix}`
505
+ );
506
+ }
507
+ };
508
+ for (const name of derived.keys()) {
509
+ if (split.behaviors.has(name)) continue;
510
+ if (split.queries.has(name)) check(name, "query", "components");
511
+ else if (split.transactions.has(name)) check(name, "command", "components");
512
+ else errs.push(`components: '${name}' has no declared publish origin (queries / transactions / behaviors) to verify against`);
513
+ }
514
+ for (const [cname, contract] of Object.entries(contracts)) {
515
+ for (const [mname, m] of Object.entries(contract.methods)) {
516
+ const at = `contract ${cname}.${mname}`;
517
+ if (contract.kind === "query") {
518
+ const q = m;
519
+ if (!split.queries.has(q.operation)) errs.push(`${at}: operation '${q.operation}' is not in queries`);
520
+ else check(q.operation, "query", at);
521
+ } else {
522
+ const cm = m;
523
+ for (const target of [cm.single, cm.batch]) {
524
+ if (target === void 0) continue;
525
+ if (target.mode === "transaction" || target.mode === "batchWrite") {
526
+ if (!split.transactions.has(target.transaction)) errs.push(`${at}: transaction '${target.transaction}' is not in transactions`);
527
+ else check(target.transaction, "command", at);
528
+ } else if (!split.commands.has(target.operation)) {
529
+ errs.push(`${at}: operation '${target.operation}' is not in commands`);
530
+ }
531
+ }
532
+ if (cm.returnSelection !== void 0) check(`${cname}__${mname}__readback`, "query", `${at} readback`);
533
+ }
534
+ }
535
+ }
536
+ if (errs.length > 0) {
537
+ throw new Error(
538
+ `graphddb CQRS classification: the declared publish split contradicts the graph-derived truth (${errs.length}):
539
+ - ${errs.join("\n - ")}`
540
+ );
541
+ }
542
+ }
543
+
544
+ // src/executor/handler-registry.ts
545
+ function resolveReadHandler(registry, name) {
546
+ const handler = registry[name];
547
+ if (handler === void 0) {
548
+ throw new Error(
549
+ `graphddb handler registry: no handler bound for catalog component '${name}' (known: ${Object.keys(registry).join(", ")})`
550
+ );
551
+ }
552
+ return handler;
553
+ }
554
+ function assertHandlersInCatalog(registry) {
555
+ for (const name of Object.keys(registry)) {
556
+ if (catalogEntry(name) === void 0) {
557
+ throw new Error(
558
+ `graphddb handler registry: handler '${name}' has no catalog entry (every bound handler must declare a Port schema in GRAPHDDB_CATALOG)`
559
+ );
560
+ }
561
+ }
562
+ }
563
+
340
564
  // src/executor/dynamo-executor.ts
565
+ var READ_HANDLERS = {
566
+ GetItem: (docClient, operation) => executeGetItem(docClient, operation),
567
+ Query: (docClient, operation) => executeQuery(docClient, operation),
568
+ BatchGetItem: (docClient, operation) => {
569
+ const op = operation;
570
+ return executeBatchGet(docClient, op);
571
+ }
572
+ };
573
+ assertHandlersInCatalog(READ_HANDLERS);
341
574
  var DynamoExecutor = class {
342
575
  async execute(operation, _options) {
343
576
  const docClient = await ClientManager.getDocumentClient();
344
- if (operation.type === "GetItem") {
345
- return executeGetItem(docClient, operation);
346
- }
347
- if (operation.type === "BatchGetItem") {
348
- return this.batchGet(operation);
349
- }
350
- return executeQuery(docClient, operation);
577
+ const handler = resolveReadHandler(READ_HANDLERS, operation.type);
578
+ return handler(docClient, operation);
351
579
  }
352
580
  async batchGet(input, _options) {
353
581
  const docClient = await ClientManager.getDocumentClient();
@@ -1842,9 +2070,21 @@ function buildUpdateInput(modelClass, entity, changes, options) {
1842
2070
  const embeddedFieldNames = new Set(
1843
2071
  meta.embeddedFields.map((e) => e.propertyName)
1844
2072
  );
1845
- const updateResult = buildUpdateExpression(serializedChanges, embeddedFieldNames);
1846
- const names = { ...updateResult.names };
1847
- const values = { ...updateResult.values };
2073
+ const addEntries = Object.entries(options?.add ?? {});
2074
+ const addNames = {};
2075
+ const addValues = {};
2076
+ const addClauses = [];
2077
+ addEntries.forEach(([field, delta], i) => {
2078
+ const nameKey = `#add${i}`;
2079
+ const valueKey = `:add${i}`;
2080
+ addNames[nameKey] = field;
2081
+ addValues[valueKey] = delta;
2082
+ addClauses.push(`${nameKey} ${valueKey}`);
2083
+ });
2084
+ const hasChanges = Object.keys(serializedChanges).length > 0;
2085
+ const updateResult = hasChanges || addClauses.length === 0 ? buildUpdateExpression(serializedChanges, embeddedFieldNames) : { expression: "", names: {}, values: {} };
2086
+ const names = { ...updateResult.names, ...addNames };
2087
+ const values = { ...updateResult.values, ...addValues };
1848
2088
  const gsiSetClauses = buildGsiRederiveSets(
1849
2089
  modelClass,
1850
2090
  meta,
@@ -1862,13 +2102,16 @@ function buildUpdateInput(modelClass, entity, changes, options) {
1862
2102
  Object.assign(names, condResult.names);
1863
2103
  Object.assign(values, condResult.values);
1864
2104
  }
2105
+ const setPortion = appendSetClauses(updateResult.expression, gsiSetClauses);
2106
+ const addPortion = addClauses.length > 0 ? `ADD ${addClauses.join(", ")}` : "";
2107
+ const updateExpression = [setPortion, addPortion].filter((s) => s !== "").join(" ");
1865
2108
  const updateInput = {
1866
2109
  TableName: tableName,
1867
2110
  Key: {
1868
2111
  PK: pk,
1869
2112
  SK: sk
1870
2113
  },
1871
- UpdateExpression: appendSetClauses(updateResult.expression, gsiSetClauses),
2114
+ UpdateExpression: updateExpression,
1872
2115
  ExpressionAttributeNames: names
1873
2116
  };
1874
2117
  if (Object.keys(values).length > 0) {
@@ -2448,6 +2691,11 @@ export {
2448
2691
  isParam,
2449
2692
  BATCH_GET_MAX_KEYS,
2450
2693
  chunkArray,
2694
+ GRAPHDDB_CATALOG,
2695
+ assertComponentsInCatalog,
2696
+ deriveContractEffect,
2697
+ deriveComponentEffects,
2698
+ assertPublishSplitMatchesDerived,
2451
2699
  ClientManager,
2452
2700
  isRawCondition,
2453
2701
  cond,