graphddb 0.5.1 → 0.5.3

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,690 @@
1
+ import { x as ParamDescriptor, y as EntityRef, z as ConditionInput, A as Param, M as ModelStatic, w as DDBModel, Q as QueryModelContract, G as QueryMethodSpec, H as CommandModelContract, I as CommandMethodSpec, J as ContractSpec, K as QuerySpec, L as CommandSpec, N as TransactionSpec, O as ContextSpec, X as DefinitionMap, Y as OperationsDocument, Z as AnyOperationDefinition, _ as Manifest, $ as BridgeBundle, a0 as ConditionSpec } from './maintenance-view-adapter-D5t9taTE.js';
2
+ import { M as MetadataRegistry } from './registry-BD_5Rm5C.js';
3
+
4
+ /**
5
+ * Declarative transaction definition DSL (issue #46, Python-bridge Phase 4).
6
+ *
7
+ * `defineTransaction(params, (tx, p) => { … })` captures a **single source of
8
+ * truth** for a `TransactWriteItems` batch: a parameter map plus a declarative
9
+ * instruction list (`tx.put` / `tx.update` / `tx.delete`, plus `tx.forEach` over
10
+ * an array param). The callback is evaluated at definition time with sentinel
11
+ * proxies in place of params / loop elements, so the recorded instructions carry
12
+ * **field-reference templates** (`{groupId}`, `{item.userId}`) rather than
13
+ * concrete values — exactly the shape the static planner (#42) turns into a
14
+ * serializable `transaction` op the TS and Python runtimes expand identically.
15
+ *
16
+ * ## Declarativity boundary (enforced — same soundness as #42 keys)
17
+ *
18
+ * The callback body may only:
19
+ * - call `tx.put/update/delete(Model, …)` with **field references** (from `p`
20
+ * or a `forEach` element) or **concrete literals** at scalar leaves;
21
+ * - call `tx.forEach(p.<arrayParam>, (el) => { … }, { when? })` whose body is
22
+ * itself only `tx.*` writes;
23
+ * - attach a declarative `when` (a small comparison over field refs) and/or a
24
+ * `condition` (the `{ notExists } | equality` subset, as for single writes).
25
+ *
26
+ * The same hardening that protects **key** positions (#42 `src/spec/symbolic.ts`)
27
+ * is applied to **value** positions (item / changes / condition values, `when`
28
+ * right-hand side). Value positions are in fact held to a *stricter* boundary
29
+ * than keys: a key may legitimately interpolate a field (`` `USER#${id}` ``), so
30
+ * coercion is allowed there and the differential template check is the boundary;
31
+ * a value leaf may **only** be a *direct* field reference or a concrete literal,
32
+ * so at a value position a faithful reference is **never coerced**. That single
33
+ * fact is the soundness boundary:
34
+ *
35
+ * 1. **Throwing sentinel Proxy.** Each param / element placeholder is a Proxy
36
+ * whose `get` trap whitelists only primitive coercion
37
+ * (`Symbol.toPrimitive` / `toString` / `valueOf`) and the brand / `token` /
38
+ * `origin` reads the planner needs, and **throws on every other property or
39
+ * method access**. So a *direct* transform on a value placeholder —
40
+ * `p.role.length`, `p.role[0]`, `p.role.toUpperCase()` — fails at the access
41
+ * site, exactly like a key sentinel. (Same first-line defense as #42.)
42
+ * 2. **Per-access coercion ledger (primary boundary).** Every minted field ref
43
+ * records each time it is coerced to a primitive (`Symbol.toPrimitive` /
44
+ * `toString` / `valueOf`). Because a faithful value reference is stored as
45
+ * the Proxy *object* and never coerced, **any** coercion of a value ref is
46
+ * positive proof the callback consumed the parameter into a transform, a
47
+ * branch, an interpolation, or a comparison — and the build is rejected. This
48
+ * is *access-site* accurate: it fires even when the same field is *also*
49
+ * referenced faithfully elsewhere in the item (so its token still surfaces),
50
+ * and it catches transforms that **converge to a constant or a shared marker
51
+ * fragment** — `String(p.role).slice(0,0)` → `''`, `.slice(0,3)` → a shared
52
+ * prefix, `[0]` / `.charAt(0)`, `.includes('Tx')` → a constant boolean, any
53
+ * `… + 'CONST'` built atop them — which a purely output-comparing check could
54
+ * not (the dropped field is masked by its faithful sibling, and the converged
55
+ * value is byte-identical across passes).
56
+ * 3. **Differential evaluation (secondary).** The callback is evaluated **twice**,
57
+ * each run delivering placeholders that coerce to a *different* per-field
58
+ * marker (the two markers share no common prefix and no common substring of
59
+ * length ≥ 2, and differ in length). Every recorded value leaf must be either
60
+ * (a) a faithful field reference (the Proxy object survived intact in *both*
61
+ * runs) or (b) a concrete literal byte-identical across both runs. This is a
62
+ * defense-in-depth layer behind the coercion ledger.
63
+ * 4. **Consumed-field check.** A placeholder field that the callback *accessed*
64
+ * but that never reached the recorded output — e.g. `(p.role === 'admin') ?
65
+ * 'A' : 'B'`, where `===` does **not** coerce (so the ledger does not fire),
66
+ * the comparison is `false`, and only a literal is emitted — is rejected: the
67
+ * param was silently dropped and a branch burned into the spec. (Same
68
+ * consumed-field accounting #42 uses for `kind === 'admin' ? … : …`.)
69
+ *
70
+ * Arbitrary JS, reads, value branching, arithmetic, coercion, or string
71
+ * transforms on a param / element placeholder are therefore rejected at build
72
+ * time, in **both** key and value positions — **never silently emitted as a
73
+ * wrong spec**. Unlike key positions, value positions have **no residual class**:
74
+ * because a faithful value ref is never coerced, even an identity-shaped
75
+ * transform (`String(p.x).slice(0)`, `.normalize()`, `.trim()`, `.replace('§',…)`
76
+ * on an absent substring) is rejected (it coerced the ref). The only operations
77
+ * that *do not* coerce a ref are (a) storing it directly and (b) the brand read;
78
+ * (a) is the supported faithful reference and (b) is internal — so the ledger has
79
+ * no false positives and no escape.
80
+ */
81
+
82
+ declare const TX_REF_BRAND: unique symbol;
83
+ /**
84
+ * A captured reference to a parameter (`{name}`) or a `forEach` element field
85
+ * (`{item.<field>}`). Branded so the planner can distinguish a reference from a
86
+ * concrete literal, and so a stray reference cannot be silently coerced.
87
+ */
88
+ interface TransactionRef {
89
+ readonly [TX_REF_BRAND]: true;
90
+ /** The template token this reference renders to, e.g. `{groupId}`. */
91
+ readonly token: string;
92
+ /** Human-readable origin (for error messages), e.g. `param 'groupId'`. */
93
+ readonly origin: string;
94
+ }
95
+ /** Runtime guard: is `value` a {@link TransactionRef}? */
96
+ declare function isTransactionRef(value: unknown): value is TransactionRef;
97
+ /** A declarative comparison used by `when` (the only branching allowed). */
98
+ type WhenComparison = {
99
+ readonly op: 'eq';
100
+ readonly left: TransactionRef;
101
+ readonly right: unknown;
102
+ } | {
103
+ readonly op: 'ne';
104
+ readonly left: TransactionRef;
105
+ readonly right: unknown;
106
+ };
107
+ /**
108
+ * A single captured instruction inside a transaction. The three write operations
109
+ * (`put` / `update` / `delete`) mutate an item; `conditionCheck` (issue #81) is a
110
+ * **read-only assertion** on another keyed item — it carries a `key` and a
111
+ * **required** `condition` and never mutates, but its failure rolls the whole
112
+ * transaction back. It is the foundation for referential-integrity derivation
113
+ * (`requires <Entity> exists` → a `conditionCheck` with `attributeExists`).
114
+ */
115
+ interface TxWriteInstruction {
116
+ readonly kind: 'write';
117
+ readonly operation: 'put' | 'update' | 'delete' | 'conditionCheck';
118
+ readonly entity: EntityRef;
119
+ /** Put: the item structure (field → ref / literal). */
120
+ readonly item?: Record<string, unknown>;
121
+ /** Update / delete / conditionCheck: the key structure. */
122
+ readonly key?: Record<string, unknown>;
123
+ /** Update: the changes structure. */
124
+ readonly changes?: Record<string, unknown>;
125
+ /**
126
+ * The write / assertion condition (subset). Optional on `put` / `update` /
127
+ * `delete`; **required** on a `conditionCheck` (the assertion it makes).
128
+ */
129
+ readonly condition?: ConditionInput;
130
+ /** Optional declarative guard (skip this item when it does not hold). */
131
+ readonly when?: WhenComparison;
132
+ }
133
+ /** A `forEach` block expanding `body` once per element of an array param. */
134
+ interface TxForEachInstruction {
135
+ readonly kind: 'forEach';
136
+ /** The array-param name iterated. */
137
+ readonly source: string;
138
+ /** Optional per-element declarative guard. */
139
+ readonly when?: WhenComparison;
140
+ /** The (write-only) instructions emitted per element. */
141
+ readonly body: TxWriteInstruction[];
142
+ }
143
+ type TxInstruction = TxWriteInstruction | TxForEachInstruction;
144
+ /** Options accepted by `tx.put/update/delete`. */
145
+ interface TxWriteOptions {
146
+ readonly condition?: ConditionInput;
147
+ readonly when?: WhenComparison;
148
+ }
149
+ /**
150
+ * Options accepted by `tx.conditionCheck` (issue #81). The `condition` is the
151
+ * read-only assertion the check makes against the keyed item and is **required**
152
+ * — a ConditionCheck with no condition is meaningless. An optional `when` skips
153
+ * the check entirely when the guard does not hold.
154
+ */
155
+ interface TxConditionCheckOptions {
156
+ readonly condition: ConditionInput;
157
+ readonly when?: WhenComparison;
158
+ }
159
+ /** Options accepted by `tx.forEach`. */
160
+ interface TxForEachOptions {
161
+ readonly when?: WhenComparison;
162
+ }
163
+ /** A small comparison helper: `when.eq(ref, value)` / `when.ne(ref, value)`. */
164
+ declare const when: {
165
+ readonly eq: (left: TransactionRef, right: unknown) => WhenComparison;
166
+ readonly ne: (left: TransactionRef, right: unknown) => WhenComparison;
167
+ };
168
+ /** The transaction recorder handed to the callback as `tx`. */
169
+ interface TxRecorder {
170
+ put(model: ModelStatic<DDBModel>, item: Record<string, unknown>, options?: TxWriteOptions): void;
171
+ update(model: ModelStatic<DDBModel>, key: Record<string, unknown>, changes: Record<string, unknown>, options?: TxWriteOptions): void;
172
+ delete(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options?: TxWriteOptions): void;
173
+ /**
174
+ * Record a **read-only** `ConditionCheck` assertion on a keyed item (issue
175
+ * #81): the item is not mutated, but `options.condition` must hold or the whole
176
+ * transaction is cancelled atomically. The foundation for referential-integrity
177
+ * derivation (`requires <Entity> exists` → `conditionCheck(Model, key, {
178
+ * condition: { attributeExists: 'PK' } })`).
179
+ */
180
+ conditionCheck(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options: TxConditionCheckOptions): void;
181
+ forEach<E>(source: E, body: (element: ElementProxy<E>) => void, options?: TxForEachOptions): void;
182
+ }
183
+ /** The element proxy type inside a `forEach` body (fields → refs). */
184
+ type ElementProxy<E> = E extends Param<infer T> ? T extends (infer Item)[] ? {
185
+ readonly [K in keyof Item]: TransactionRef;
186
+ } : never : never;
187
+ /** The typed IR node produced by {@link defineTransaction}. */
188
+ interface TransactionDefinition {
189
+ /** @internal Marks this object as a transaction definition IR node. */
190
+ readonly __isTransactionDefinition: true;
191
+ /** Collected parameters: name → descriptor (array params carry `element`). */
192
+ readonly params: Readonly<Record<string, ParamDescriptor>>;
193
+ /** The captured, declarative instruction list. */
194
+ readonly instructions: readonly TxInstruction[];
195
+ }
196
+ /** A param map accepted by {@link defineTransaction}. */
197
+ type TransactionParamShape = Record<string, Param<unknown>>;
198
+ /** The `p` proxy type: each scalar param → ref; each array param → itself. */
199
+ type ParamProxy<P extends TransactionParamShape> = {
200
+ readonly [K in keyof P]: P[K] extends {
201
+ readonly kind: 'array';
202
+ } ? P[K] : TransactionRef;
203
+ };
204
+ /**
205
+ * Define a declarative transaction. The callback is evaluated **twice** with
206
+ * throwing value sentinels (one differential marker set per pass); its `tx.*`
207
+ * calls are recorded and the two passes are checked against each other so any
208
+ * non-declarative value (a transform, a value branch, a coercion escape) is
209
+ * rejected at build time — the same soundness #42 enforces for key positions.
210
+ *
211
+ * @param params A record of `param.*` placeholders (scalars and/or `param.array`).
212
+ * @param build `(tx, p) => void` — records the write instructions.
213
+ */
214
+ declare function defineTransaction<const P extends TransactionParamShape>(params: P, build: (tx: TxRecorder, p: ParamProxy<P>) => void): TransactionDefinition;
215
+ /**
216
+ * Group a record of transaction definitions into the IR consumed by the static
217
+ * planner (#42/#46). Validates each entry is a transaction definition.
218
+ */
219
+ declare function defineTransactions<const D extends Record<string, TransactionDefinition>>(definitions: D): D;
220
+
221
+ /**
222
+ * Contract-layer serialization (issue #59, CQRS Contract layer, Epic #57;
223
+ * spec `docs/cqrs-contract.md`, "SSoT Schema").
224
+ *
225
+ * The Contract IR (#58, `src/define/contract.ts`) holds, per contract method, a
226
+ * resolved declarative {@link ContractMethodOp} plus the **decided facts** the
227
+ * planner derived (`resolution` / `inputArity` / `result`). This module
228
+ * **serializes** those facts into the JSON SSoT — it does **not** re-derive them:
229
+ *
230
+ * - The `contracts` map (contract name → {@link ContractSpec}) is layered **on
231
+ * top of** the existing `queries` / `commands` / `transactions`. Each method
232
+ * references one of them by name (`ContractName__methodName`), so the existing
233
+ * operation-spec shapes are unchanged and the current runtime keeps working.
234
+ * - The referenced operation spec is produced by **reusing** the proven
235
+ * {@link buildQuerySpec} / {@link buildCommandSpec} planners: a
236
+ * {@link ContractMethodOp} is translated back into the {@link AnyOperationDefinition}
237
+ * shape those planners consume (the contract's sentinel references become the
238
+ * `Param` placeholders the planner already understands), so the symbolic key
239
+ * evaluation, projection, relation chaining, and condition handling are shared
240
+ * — never re-implemented, never drifting.
241
+ *
242
+ * Everything emitted is **JSON-serializable** by construction (only strings /
243
+ * numbers / booleans / arrays / plain objects). A contract-free input emits no
244
+ * `contracts` / `contexts` keys, so the pre-#59 operations document is reproduced
245
+ * byte-for-byte (backward compatibility).
246
+ *
247
+ * ## What this issue serializes vs. defers
248
+ *
249
+ * The #58 IR captures a single declarative op per method with a boolean `select`
250
+ * projection; it does **not** model External Query / Query Composition (no
251
+ * `query(Contract.method, { from(...) })` primitive exists yet). So no #58
252
+ * contract produces a `compose`. The serializer nonetheless **supports** it: when
253
+ * a method op carries composition (a forward-compatible {@link ContractMethodOp}
254
+ * extension), it is emitted per the proposal's `compose` shape. The N+1 checker
255
+ * (#60), boundary lint (#61), and runtime execution (#62) are out of scope here —
256
+ * this is serialization + generator only.
257
+ */
258
+
259
+ /**
260
+ * A contract as accepted by the serializer: either a resolved
261
+ * {@link QueryModelContract} or a {@link CommandModelContract} (the #58 factory
262
+ * output). The Key type is erased — the serializer reads the key **fields** off
263
+ * the resolved op / model metadata, not the phantom type.
264
+ */
265
+ type AnyModelContract = QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
266
+ /** A map of contract name → resolved contract IR (as passed to the generator). */
267
+ type ContractMap = Record<string, AnyModelContract>;
268
+ /**
269
+ * A context-ownership declaration as authored by the producer: context name →
270
+ * the Model (entity) and Contract names that belong to it. The serializer sorts
271
+ * the member lists deterministically; see {@link ContextSpec}.
272
+ */
273
+ interface ContextOwnership {
274
+ readonly models?: readonly string[];
275
+ readonly contracts?: readonly string[];
276
+ }
277
+ /** A map of context name → its membership (as passed to the generator). */
278
+ type ContextOwnershipMap = Record<string, ContextOwnership>;
279
+ /**
280
+ * The output of {@link buildContracts}: the `contracts` map and the
281
+ * **synthesized** operation specs each method references. The caller merges the
282
+ * synthesized `queries` / `commands` / `transactions` into the document's
283
+ * existing maps (a contract method's referenced op lives alongside hand-written
284
+ * definitions, addressed by name).
285
+ */
286
+ interface BuiltContracts {
287
+ readonly contracts: Record<string, ContractSpec>;
288
+ /** Synthesized read ops referenced by query contract methods. */
289
+ readonly queries: Record<string, QuerySpec>;
290
+ /** Synthesized write ops referenced by command contract methods (single form). */
291
+ readonly commands: Record<string, CommandSpec>;
292
+ /**
293
+ * Synthesized transactions referenced by command contract methods whose array
294
+ * form resolves to a `'transact'` batched write (issue #64). One transaction per
295
+ * such method, named `<Contract>__<method>__batch`: a single `forEach`-over-keys
296
+ * write item that the runtime expands once per key (≤25, atomic), reusing the
297
+ * proven declarative-transaction machinery (#46). Empty when no command method
298
+ * declares a `'transact'` batch.
299
+ */
300
+ readonly transactions: Record<string, TransactionSpec>;
301
+ }
302
+ /**
303
+ * Serialize a map of resolved contracts into the `contracts` SSoT map plus the
304
+ * synthesized operation specs each method references. Deterministic: contracts
305
+ * and methods are emitted in sorted-key order.
306
+ *
307
+ * @param contracts Contract name → resolved {@link QueryModelContract} /
308
+ * {@link CommandModelContract} (the #58 factory output). Empty → empty output.
309
+ * @throws if a contract method op cannot be serialized (a non-boolean projection
310
+ * slot, an unsupported write condition); or if a contract violates an N+1
311
+ * safety rule (#60, {@link assertContractN1Safe}) — an array into a `range`
312
+ * method, a "list under a list", or a `range` composed child.
313
+ */
314
+ declare function buildContracts(contracts?: ContractMap): BuiltContracts;
315
+ /**
316
+ * Serialize the context-ownership declarations into the SSoT `contexts` map.
317
+ * Member lists are sorted for determinism. An empty input yields an empty map
318
+ * (the caller then omits the key for backward compatibility).
319
+ */
320
+ declare function buildContexts(contexts?: ContextOwnershipMap): Record<string, ContextSpec>;
321
+
322
+ /**
323
+ * Static parameterized operation-spec generation (issue #42, Phase 0b).
324
+ *
325
+ * Turns the parameterized definition IR (#41) into JSON-serializable
326
+ * {@link OperationSpec} / {@link CommandSpec} via **symbolic evaluation** of the
327
+ * key / GSI mapping functions. The result is deterministic and contains only
328
+ * `{param}` / `{result.field}` template placeholders, never concrete values.
329
+ *
330
+ * Relation chains in a read `select` become **multiple operations** wired with
331
+ * `resultPath` and `{result.*}` key templates, mirroring the runtime
332
+ * `relation-planner` policy: `hasMany` → `Query` + `begins_with`, `belongsTo` /
333
+ * `hasOne` → `BatchGetItem`.
334
+ */
335
+
336
+ /** Build the {@link QuerySpec} for a single read definition. */
337
+ declare function buildQuerySpec(def: AnyOperationDefinition): QuerySpec;
338
+ /**
339
+ * Optional contract-layer inputs to {@link buildOperations} (issue #59). Both are
340
+ * absent on a pre-#59 (contract-free) build, in which case the produced document
341
+ * is byte-identical to the pre-#59 shape (no `contracts` / `contexts` keys).
342
+ */
343
+ interface ContractInputs {
344
+ /** Resolved contracts (publicQueryModel / publicCommandModel output). */
345
+ readonly contracts?: ContractMap;
346
+ /** Context-ownership declarations (context → member models / contracts). */
347
+ readonly contexts?: ContextOwnershipMap;
348
+ }
349
+ /**
350
+ * Build the operations document from grouped read (`queries`) and write
351
+ * (`commands`) definition maps, plus the optional CQRS contract layer (#59).
352
+ * All inputs are optional. Output is deterministic (definitions emitted in
353
+ * sorted-key order; the runtime resolves by name).
354
+ *
355
+ * The contract layer (#59) is layered **on top of** the existing specs: each
356
+ * contract method's underlying op is synthesized into `queries` / `commands` and
357
+ * referenced by name, and the `contracts` / `contexts` maps are emitted. A
358
+ * contract-free input (`contractInputs` empty / absent) reproduces the pre-#59
359
+ * document byte-for-byte — no `contracts` / `contexts` keys appear.
360
+ *
361
+ * @throws if a command carries a write condition outside the supported subset
362
+ * (`{ notExists }` or pure equality) — the bridge guard; or if a contract op
363
+ * name collides with a hand-written definition.
364
+ */
365
+ declare function buildOperations(queries?: DefinitionMap, commands?: DefinitionMap, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): OperationsDocument;
366
+
367
+ /**
368
+ * Manifest generation (issue #42, Python-bridge Phase 0b).
369
+ *
370
+ * Produces the JSON-serializable {@link Manifest} from a {@link MetadataRegistry}
371
+ * snapshot: entity → table / physicalName / prefix / field types / key & GSI
372
+ * templates / relation metadata. Generation is **deterministic** — entities,
373
+ * fields, GSIs, and relations are emitted in a stable (sorted) order so the same
374
+ * registry always yields byte-identical JSON.
375
+ */
376
+
377
+ /**
378
+ * Build the full {@link Manifest} from the {@link MetadataRegistry}.
379
+ *
380
+ * Every registered entity is included. Output ordering is deterministic
381
+ * (entities / fields / relations sorted by name, GSIs by index name).
382
+ */
383
+ declare function buildManifest(registry?: typeof MetadataRegistry): Manifest;
384
+
385
+ /**
386
+ * N+1 static checker for the CQRS Contract layer (issue #60, Epic #57; proposal
387
+ * `docs/cqrs-contract.md`, "N+1 Safety (Static Restriction)").
388
+ *
389
+ * Whether a resolution is N+1-safe depends on **two** facts the planner already
390
+ * decided in #58 and #59 serialized into the SSoT — the checker **honors** them,
391
+ * it does **not** re-derive policy ("decide once, declare in the SSoT"):
392
+ *
393
+ * - `resolution` — `'point'` (target keys known → `BatchGetItem` / another
394
+ * contract's `get(keys[])`; coalesces) or `'range'` (target key set unknown →
395
+ * partition `Query`; one request per partition key).
396
+ * - `inputArity` / parent `cardinality` — how many keys are fed in. A `range`
397
+ * method is one query for one key but N queries for N keys.
398
+ *
399
+ * The rule (absolute, no opt-in escape hatch): **a `range` resolution is allowed
400
+ * only with single-key input.** Three forms are build-time errors:
401
+ *
402
+ * (a) **array into a `range` method** — a `range` method whose `inputArity` is
403
+ * not `'single'` (`'array'` / `'either'`). A `range` method must be
404
+ * `'single'`; anything else is an N partition-`Query` fan-out.
405
+ * (a′) **array into a unique-GSI `point` method** (issue #71) — a `point` method
406
+ * whose Key resolves via a **unique GSI** (a per-key `Query`, since
407
+ * `BatchGetItem` cannot read a GSI) but whose `inputArity` is not `'single'`.
408
+ * A base-table `point` coalesces a key array into one `BatchGetItem`, but a
409
+ * unique-GSI `point` would issue N `Query`s — an N+1 fan-out, the same shape
410
+ * as (a). graphddb's official spec does not include N+1-producing queries, so
411
+ * it is rejected at build time; the application-side loop is the escape (same
412
+ * philosophy as `range`).
413
+ * (b) **"list under a list"** — a `range` child nested under a parent step whose
414
+ * output cardinality is `'many'`. The composition's parent yields N records,
415
+ * so a `range` child fans out to N partition `Query`s.
416
+ * (c) **`compose` child that is `range`** — the cross-contract / composition form
417
+ * of (b): a composed (External Query) child declared `range` under a parent
418
+ * that can yield many. (Rule (b) and (c) are the same axis — a `range` child
419
+ * under a many-yielding parent — surfaced through nested model relations vs.
420
+ * cross-contract composition respectively.)
421
+ *
422
+ * `point` resolutions are always allowed (they coalesce). If a caller genuinely
423
+ * needs "just these few", it loops in its own application code (the proposal's
424
+ * `Promise.all(ids.map(...))`), keeping the N visible at the call site —
425
+ * the contract surface never hides a fan-out.
426
+ *
427
+ * ## Placement — build path, not the model `LintRule` framework
428
+ *
429
+ * This checker runs on the **contract build path** ({@link buildContracts} in
430
+ * `src/spec/contracts.ts`), where every other contract validation surfaces
431
+ * (#59's projection audit, write-condition guard, and the original inline
432
+ * `compose` rule all `throw` there). It operates on the serialized
433
+ * {@link ContractSpec} facts, not on {@link EntityMetadata}: the existing
434
+ * `src/linter/` `LintRule` framework is keyed on a single model's `EntityMetadata`
435
+ * (model relations / GSIs), which carries none of the contract `resolution` /
436
+ * `inputArity` / `compose` facts the N+1 rule needs. Surfacing the violation as a
437
+ * thrown build error — exactly as `relation/traversal.ts` `validateDepth` and the
438
+ * #59 contract validations do — fails the build the same way `inputArity`
439
+ * mismatches and `range` composed children already failed it.
440
+ *
441
+ * The cross-contract `range`-child form (c) and the nested-relation form (b) are
442
+ * only fully reachable once the composition DSL primitive
443
+ * (`query(Contract.method, { from() })`, owned by #63) lands; until then no #58
444
+ * DSL produces a `compose` node. The checker implements the full logic now so it
445
+ * is ready, and the forward forms are exercised by feeding synthesized SSoT nodes
446
+ * (exactly as #59's serializer tests do).
447
+ */
448
+
449
+ /** A single N+1 rule violation, carrying the rule label and a clear message. */
450
+ interface ContractN1Violation {
451
+ /** Which of the N+1 forms was violated. */
452
+ readonly rule: 'array-into-range' | 'array-into-gsi-point' | 'list-under-list' | 'compose-range-under-many';
453
+ /** The offending contract name. */
454
+ readonly contract: string;
455
+ /** The offending method name. */
456
+ readonly method: string;
457
+ /** A clear, actionable error message (the same text the thrown error carries). */
458
+ readonly message: string;
459
+ }
460
+ /**
461
+ * Tells, for a query contract method name, whether its `point` read resolves via
462
+ * a **unique GSI** (a per-key `Query`) rather than the base-table primary key (a
463
+ * coalescible `GetItem` / `BatchGetItem`). Supplied by the serializer (issue #71),
464
+ * which recovers the fact from the method's synthesized read op (a root `Query`
465
+ * carrying an `indexName`). Absent → no method is treated as a GSI point (the
466
+ * pre-#71 default; e.g. synthesized-SSoT checker tests that exercise only the
467
+ * `range` / `compose` rules).
468
+ */
469
+ type IsGsiPointMethod = (methodName: string) => boolean;
470
+ /**
471
+ * Collect every N+1 violation in a single serialized {@link ContractSpec}.
472
+ * Command contracts carry no `resolution` / `compose` and are always N+1-safe
473
+ * (writes resolve to known keys), so only query contracts are inspected.
474
+ *
475
+ * @param isGsiPoint Optional predicate (issue #71): does the named method's
476
+ * `point` read resolve via a unique GSI? Used to reject array-into-GSI-point
477
+ * (rule a′). Defaults to "never" when not supplied.
478
+ */
479
+ declare function collectContractN1Violations(contractName: string, spec: ContractSpec, isGsiPoint?: IsGsiPointMethod): ContractN1Violation[];
480
+ /**
481
+ * Assert a single serialized contract is N+1-safe, throwing the first violation
482
+ * as a build error (mirroring `relation/traversal.ts` `validateDepth` and the
483
+ * #59 contract validations, which `throw` on the build path). Multiple violations
484
+ * are summarized in the thrown message so the build surfaces them all at once.
485
+ *
486
+ * @throws if any of the N+1 forms is present.
487
+ *
488
+ * @param isGsiPoint Optional predicate (issue #71) identifying unique-GSI `point`
489
+ * methods, so rule (a′) (array into a GSI point) can be enforced.
490
+ */
491
+ declare function assertContractN1Safe(contractName: string, spec: ContractSpec, isGsiPoint?: IsGsiPointMethod): void;
492
+
493
+ /**
494
+ * Bounded-context boundary checker for the CQRS Contract layer (issue #61, Epic
495
+ * #57; spec `docs/cqrs-contract.md`, External Query section "Why it
496
+ * exists: boundary enforcement").
497
+ *
498
+ * The rule, stated in the proposal and the issue: a **Model is a private
499
+ * implementation detail of its bounded context; a Contract is the published
500
+ * interface**. Within its own context a contract resolves its own Models directly
501
+ * (internal `query` / `list` / `put` / `update` / `delete`). Across a context
502
+ * boundary it may depend on another context **only through that context's
503
+ * published Contract** (an External Query / Command). Reaching directly into a
504
+ * **foreign** Model is a **build-time error** — it couples one context to
505
+ * another's private storage shape and silently defeats the boundary the contexts
506
+ * declaration is meant to enforce.
507
+ *
508
+ * ## What "own" vs. "foreign" means here
509
+ *
510
+ * The decision is made entirely from the **context-ownership declaration** (#59,
511
+ * the `contexts` map: context name → `{ models, contracts }`). For each contract:
512
+ *
513
+ * - its **owning context** is the (unique) context whose `contracts` list names
514
+ * it; and
515
+ * - for every Model its method ops touch (the recorded {@link ContractMethodOp}
516
+ * `entity` refs, #58), that Model's **owning context** is the context whose
517
+ * `models` list names it.
518
+ *
519
+ * A method op against a Model owned by a **different** context than the contract's
520
+ * own is the violation. Three cases are deliberately **allowed**, never flagged:
521
+ *
522
+ * - the Model and the contract are in the **same** context (the normal internal
523
+ * case);
524
+ * - the **contract is in no declared context** — there is no boundary to enforce
525
+ * *from*, so the contract is unconstrained (a partial / incremental adoption of
526
+ * the contexts declaration must not turn unrelated contracts into errors);
527
+ * - the touched **Model is in no declared context** — an undeclared Model has no
528
+ * owner to protect, so it is not "foreign" to anyone. (Declaring the Model into
529
+ * a context is what opts it into boundary protection.)
530
+ *
531
+ * In short: the check fires **only** when both the contract *and* the foreign
532
+ * Model have declared, *different* owners. This is the least-surprising reading of
533
+ * a *partial* contexts declaration and keeps the rule purely additive — declaring
534
+ * more ownership can only ever surface *more* violations, never fewer.
535
+ *
536
+ * ## Placement — build path, not the model `LintRule` framework (decision B)
537
+ *
538
+ * This checker runs in **{@link buildOperations}** (`src/spec/operations.ts`),
539
+ * immediately before the {@link buildContracts} call — the seam where both the
540
+ * raw contract IR (the recorded op `entity` refs) and the `contexts` map are
541
+ * still available, alongside #59's projection audit, the write-condition guard,
542
+ * and #60's N+1 checker — every contract validation surfaces there as a thrown
543
+ * build error. It is **not** added to the `src/linter/` `LintRule`
544
+ * framework, even though the issue text suggests a `context-boundary` rule there,
545
+ * because that framework's contract — `check(metadata: EntityMetadata, registry)`
546
+ * — is keyed on a **single model's** {@link EntityMetadata} and carries **none** of
547
+ * the facts this rule needs:
548
+ *
549
+ * - which context a *contract* belongs to (a contract is not an `EntityMetadata`);
550
+ * - which Models a contract's *methods* touch (these live on the unserialized
551
+ * {@link ContractMethodOp} `entity` refs, not on any model's metadata);
552
+ * - the cross-cutting `contexts` map relating the two.
553
+ *
554
+ * A `LintRule` sees one model at a time and no contract facts at all, so a naive
555
+ * rule physically cannot observe a contract→foreign-Model reference. Bending the
556
+ * framework to carry contracts + contexts would be an invasive, parallel
557
+ * "rule kind" that no other rule uses — exactly the ugly refactor the brief warns
558
+ * against. Placing the check here is consistent with how #60 already resolved the
559
+ * identical tension (`src/spec/contract-n1-check.ts`, "Placement"), and it reads
560
+ * the **same facts the rule actually depends on** at the one point where contracts
561
+ * and contexts are both in hand. The boundary violation fails the build the same
562
+ * way an `inputArity` mismatch or a `range` composed child already does.
563
+ */
564
+
565
+ /** A single context-boundary violation, carrying the offending facts + message. */
566
+ interface ContractBoundaryViolation {
567
+ /** The offending contract name. */
568
+ readonly contract: string;
569
+ /** The context that owns the offending contract. */
570
+ readonly contractContext: string;
571
+ /** The contract method whose op touched the foreign Model. */
572
+ readonly method: string;
573
+ /** The foreign Model (entity / model-class name) referenced directly. */
574
+ readonly foreignModel: string;
575
+ /** The context that owns the foreign Model. */
576
+ readonly foreignContext: string;
577
+ /** A clear, actionable error message (the same text the thrown error carries). */
578
+ readonly message: string;
579
+ }
580
+ /**
581
+ * Collect every context-boundary violation across a map of resolved contracts,
582
+ * given the context-ownership declaration. A violation is a contract method whose
583
+ * recorded op touches a Model owned by a **different** context than the contract's
584
+ * own (see the module doc for the precise own / foreign / undeclared rule).
585
+ *
586
+ * @param contracts Contract name → resolved {@link QueryModelContract} /
587
+ * {@link CommandModelContract} (the #58 factory output).
588
+ * @param contexts The context-ownership declaration (#59). When empty, no
589
+ * contract has a declared owner, so there are no boundaries to enforce → no
590
+ * violations.
591
+ * @throws if the contexts declaration claims a Model / Contract in two contexts
592
+ * (an ambiguous, self-contradictory declaration).
593
+ */
594
+ declare function collectContractBoundaryViolations(contracts: ContractMap, contexts: ContextOwnershipMap): ContractBoundaryViolation[];
595
+ /**
596
+ * Assert a map of resolved contracts respects bounded-context boundaries, throwing
597
+ * the first violation as a build error (mirroring #60's {@link assertContractN1Safe}
598
+ * and the other #59 contract validations, which `throw` on the build path).
599
+ * Multiple violations are summarized in the thrown message so the build surfaces
600
+ * them all at once.
601
+ *
602
+ * @param contracts Contract name → resolved contract IR.
603
+ * @param contexts The context-ownership declaration (#59).
604
+ * @throws if any contract directly references a foreign context's Model, or if the
605
+ * contexts declaration is ambiguous (a Model / Contract claimed twice).
606
+ */
607
+ declare function assertContractBoundaries(contracts: ContractMap, contexts: ContextOwnershipMap): void;
608
+
609
+ /**
610
+ * Static planning for declarative transactions (issue #46, Phase 4).
611
+ *
612
+ * Turns a {@link TransactionDefinition} (captured by `defineTransaction`) into a
613
+ * serializable {@link TransactionSpec}. Each `tx.put/update/delete` becomes a
614
+ * templated {@link TransactionItemSpec}; a `tx.forEach` block marks its items
615
+ * with a `forEach` binding so the runtime expands them once per array-param
616
+ * element. Key templates are derived with the **same** symbolic key-mapping
617
+ * evaluator (#42 `evaluateKey`) the single-write planner uses — the
618
+ * declarativity of the field references is what lets that reuse work: every
619
+ * key / item / changes leaf is either a field reference (whose token the planner
620
+ * substitutes) or a concrete literal.
621
+ */
622
+
623
+ /** Build the {@link TransactionSpec} for one transaction definition. */
624
+ declare function buildTransactionSpec(txName: string, def: TransactionDefinition): TransactionSpec;
625
+ /** Build the transactions map from a record of transaction definitions. */
626
+ declare function buildTransactions(transactions?: Record<string, TransactionDefinition>): Record<string, TransactionSpec>;
627
+
628
+ /**
629
+ * Bridge guard (issue #42, Phase 0b).
630
+ *
631
+ * The Python bridge can only carry a **declarative, serializable subset** of
632
+ * graphddb. This module enforces that subset at build time:
633
+ *
634
+ * 1. **Write conditions** must be `{ notExists }`, an existence primitive
635
+ * (`{ attributeExists }` / `{ attributeNotExists }` on any field, issue #81),
636
+ * *or* pure field equality (mirroring `expression/condition-expression.ts`,
637
+ * which only emits `attribute_(not_)exists(...)` or `#f = :v` clauses). Any
638
+ * other condition shape is rejected — there is no serializable representation
639
+ * for it. {@link conditionInputToSpec} is the single place the IR-shaped input
640
+ * is mapped to the {@link ConditionSpec} subset.
641
+ * 2. The produced bundle must be **JSON-serializable** — no functions, classes,
642
+ * `Date`, `Set`, `Map`, `Symbol`, or non-finite numbers may leak into the
643
+ * spec. {@link assertJsonSerializable} walks the value and throws on any.
644
+ *
645
+ * (Note: the post-load TS predicate `refine` was removed in #49, so no guard for
646
+ * it is needed; see `docs/python-bridge.md`.)
647
+ */
648
+
649
+ /**
650
+ * Assert a write {@link ConditionSpec} is within the supported subset. The spec
651
+ * type already constrains it to `notExists` | `attributeExists` |
652
+ * `attributeNotExists` | `equals`, so this defends against a malformed `equals`
653
+ * (empty / non-template values) or a blank existence field and gives a clear,
654
+ * single-source build-time error message for any future widening.
655
+ */
656
+ declare function assertSupportedCondition(commandName: string, condition: ConditionSpec): void;
657
+ /**
658
+ * Walk an arbitrary value and throw if it contains anything that does not
659
+ * survive `JSON.stringify` round-tripping as itself (functions, symbols,
660
+ * `undefined`, `Date`, `Map`, `Set`, class instances, non-finite numbers).
661
+ */
662
+ declare function assertJsonSerializable(value: unknown, path?: string): void;
663
+ /** Convenience: assert an entire {@link BridgeBundle} is JSON-serializable. */
664
+ declare function assertBundleSerializable(bundle: BridgeBundle): void;
665
+
666
+ /**
667
+ * Static parameterized planner: serializable OperationSpec + manifest + bridge
668
+ * guard (issue #42, Python-bridge Phase 0b). Public surface.
669
+ *
670
+ * The CLI / file output is #43's responsibility; this module stops at the
671
+ * **generation functions** producing the typed, JSON-serializable
672
+ * `{ manifest, operations }` bundle.
673
+ */
674
+
675
+ /**
676
+ * Build the full `{ manifest, operations }` bridge bundle from grouped query /
677
+ * command definition maps. The bundle is typed and JSON-serializable; the bridge
678
+ * guard runs (unsupported write conditions throw during `buildOperations`, and
679
+ * the whole bundle is asserted JSON-serializable here).
680
+ *
681
+ * @param queries Read definitions (`defineQueries(...)` output). Optional.
682
+ * @param commands Write definitions (`defineCommands(...)` output). Optional.
683
+ * @param registry Metadata source (defaults to the global registry).
684
+ * @param transactions Declarative transactions (`defineTransactions(...)`). Optional.
685
+ * @param contractInputs The CQRS contract layer (#59): resolved contracts +
686
+ * context-ownership. Optional — absent reproduces the pre-#59 document.
687
+ */
688
+ declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
689
+
690
+ export { type AnyModelContract as A, type BuiltContracts as B, type ContextOwnership as C, buildTransactions as D, collectContractBoundaryViolations as E, collectContractN1Violations as F, defineTransaction as G, defineTransactions as H, isTransactionRef as I, when as J, type TransactionDefinition as T, type WhenComparison as W, type ContextOwnershipMap as a, type ContractBoundaryViolation as b, type ContractInputs as c, type ContractMap as d, type ContractN1Violation as e, type TransactionParamShape as f, type TransactionRef as g, type TxConditionCheckOptions as h, type TxForEachInstruction as i, type TxForEachOptions as j, type TxInstruction as k, type TxRecorder as l, type TxWriteInstruction as m, type TxWriteOptions as n, assertBundleSerializable as o, assertContractBoundaries as p, assertContractN1Safe as q, assertJsonSerializable as r, assertSupportedCondition as s, buildBridgeBundle as t, buildContexts as u, buildContracts as v, buildManifest as w, buildOperations as x, buildQuerySpec as y, buildTransactionSpec as z };