graphddb 0.7.10 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -6
  2. package/dist/cdc/index.d.ts +389 -5
  3. package/dist/cdc/index.js +4 -4
  4. package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
  5. package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
  6. package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
  7. package/dist/chunk-L4QRCHRQ.js +278 -0
  8. package/dist/chunk-LAT64YCZ.js +1987 -0
  9. package/dist/chunk-S2NI4PBW.js +187 -0
  10. package/dist/{chunk-EOJDN3SA.js → chunk-T44OB5GU.js} +3757 -6138
  11. package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
  12. package/dist/cli.js +63 -254
  13. package/dist/index.d.ts +23 -1550
  14. package/dist/index.js +94 -1850
  15. package/dist/internal/index.d.ts +84 -0
  16. package/dist/internal/index.js +701 -0
  17. package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DZtjAQDh.d.ts} +573 -1817
  18. package/dist/linter/index.d.ts +39 -7
  19. package/dist/linter/index.js +22 -4
  20. package/dist/{registry-LWE54Sdc.d.ts → linter-DQY7gUEk.d.ts} +22 -22
  21. package/dist/prepared-artifact-HFealr1q.d.ts +281 -0
  22. package/dist/spec/index.d.ts +506 -5
  23. package/dist/spec/index.js +22 -18
  24. package/dist/testing/index.d.ts +2 -3
  25. package/dist/testing/index.js +4 -4
  26. package/dist/transform/index.d.ts +1 -1
  27. package/dist/transform/index.js +4 -4
  28. package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
  29. package/dist/types-DW__-Icc.d.ts +450 -0
  30. package/docs/cdc-projection.md +5 -5
  31. package/docs/class-hydration.md +1 -1
  32. package/docs/cqrs-contract.md +79 -20
  33. package/docs/design-patterns.md +5 -5
  34. package/docs/docs-generation.md +6 -6
  35. package/docs/middleware.md +15 -15
  36. package/docs/mutation-command-derivation.md +52 -42
  37. package/docs/prepared-statements.md +14 -14
  38. package/docs/python-bridge.md +96 -65
  39. package/docs/spec.md +153 -124
  40. package/docs/testing.md +9 -8
  41. package/package.json +14 -4
  42. package/dist/chunk-3UD3XIF2.js +0 -860
  43. package/dist/chunk-MMVHOUM4.js +0 -24
  44. package/dist/from-change-Ty95KA8C.d.ts +0 -327
  45. package/dist/index-Dc7d8mWI.d.ts +0 -1089
  46. package/dist/relation-depth-BRS513Tq.d.ts +0 -36
@@ -1,1089 +0,0 @@
1
- import { Q as QueryModelContract, x as QueryMethodSpec, y as CommandModelContract, z as CommandMethodSpec, m as EntityMetadata, A as ParamDescriptor, G as EntityRef, H as ConditionInput, M as ModelStatic, w as DDBModel, I as DefinitionMap, J as AnyOperationDefinition, K as PreparedBody } from './maintenance-view-adapter-BAZ9uBGe.js';
2
- import { C as ContractSpec, Q as QuerySpec, a as CommandSpec, T as TransactionSpec, b as ContextSpec, S as SPEC_VERSION, P as ParamSpec, c as Param, E as ExpressionSpec, O as OperationsDocument, M as Manifest, B as BridgeBundle, d as ConditionSpec, e as SpecVersion } from './types-BQLzTEqh.js';
3
- import { M as MetadataRegistry } from './registry-LWE54Sdc.js';
4
-
5
- /**
6
- * Contract-layer serialization (issue #59, CQRS Contract layer, Epic #57;
7
- * spec `docs/cqrs-contract.md`, "SSoT Schema").
8
- *
9
- * The Contract IR (#58, `src/define/contract.ts`) holds, per contract method, a
10
- * resolved declarative {@link ContractMethodOp} plus the **decided facts** the
11
- * planner derived (`resolution` / `inputArity` / `result`). This module
12
- * **serializes** those facts into the JSON SSoT — it does **not** re-derive them:
13
- *
14
- * - The `contracts` map (contract name → {@link ContractSpec}) is layered **on
15
- * top of** the existing `queries` / `commands` / `transactions`. Each method
16
- * references one of them by name (`ContractName__methodName`), so the existing
17
- * operation-spec shapes are unchanged and the current runtime keeps working.
18
- * - The referenced operation spec is produced by **reusing** the proven
19
- * {@link buildQuerySpec} / {@link buildCommandSpec} planners: a
20
- * {@link ContractMethodOp} is translated back into the {@link AnyOperationDefinition}
21
- * shape those planners consume (the contract's sentinel references become the
22
- * `Param` placeholders the planner already understands), so the symbolic key
23
- * evaluation, projection, relation chaining, and condition handling are shared
24
- * — never re-implemented, never drifting.
25
- *
26
- * Everything emitted is **JSON-serializable** by construction (only strings /
27
- * numbers / booleans / arrays / plain objects). A contract-free input emits no
28
- * `contracts` / `contexts` keys, so the pre-#59 operations document is reproduced
29
- * byte-for-byte (backward compatibility).
30
- *
31
- * ## What this issue serializes vs. defers
32
- *
33
- * The #58 IR captures a single declarative op per method with a boolean `select`
34
- * projection; it does **not** model External Query / Query Composition (no
35
- * `query(Contract.method, { from(...) })` primitive exists yet). So no #58
36
- * contract produces a `compose`. The serializer nonetheless **supports** it: when
37
- * a method op carries composition (a forward-compatible {@link ContractMethodOp}
38
- * extension), it is emitted per the proposal's `compose` shape. The N+1 checker
39
- * (#60), boundary lint (#61), and runtime execution (#62) are out of scope here —
40
- * this is serialization + generator only.
41
- */
42
-
43
- /**
44
- * A contract as accepted by the serializer: either a resolved
45
- * {@link QueryModelContract} or a {@link CommandModelContract} (the #58 factory
46
- * output). The Key type is erased — the serializer reads the key **fields** off
47
- * the resolved op / model metadata, not the phantom type.
48
- */
49
- type AnyModelContract = QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
50
- /** A map of contract name → resolved contract IR (as passed to the generator). */
51
- type ContractMap = Record<string, AnyModelContract>;
52
- /**
53
- * A context-ownership declaration as authored by the producer: context name →
54
- * the Model (entity) and Contract names that belong to it. The serializer sorts
55
- * the member lists deterministically; see {@link ContextSpec}.
56
- */
57
- interface ContextOwnership {
58
- readonly models?: readonly string[];
59
- readonly contracts?: readonly string[];
60
- }
61
- /** A map of context name → its membership (as passed to the generator). */
62
- type ContextOwnershipMap = Record<string, ContextOwnership>;
63
- /**
64
- * The output of {@link buildContracts}: the `contracts` map and the
65
- * **synthesized** operation specs each method references. The caller merges the
66
- * synthesized `queries` / `commands` / `transactions` into the document's
67
- * existing maps (a contract method's referenced op lives alongside hand-written
68
- * definitions, addressed by name).
69
- */
70
- interface BuiltContracts {
71
- readonly contracts: Record<string, ContractSpec>;
72
- /** Synthesized read ops referenced by query contract methods. */
73
- readonly queries: Record<string, QuerySpec>;
74
- /** Synthesized write ops referenced by command contract methods (single form). */
75
- readonly commands: Record<string, CommandSpec>;
76
- /**
77
- * Synthesized transactions referenced by command contract methods whose array
78
- * form resolves to a `'transact'` batched write (issue #64). One transaction per
79
- * such method, named `<Contract>__<method>__batch`: a single `forEach`-over-keys
80
- * write item that the runtime expands once per key (≤25, atomic), reusing the
81
- * proven declarative-transaction machinery (#46). Empty when no command method
82
- * declares a `'transact'` batch.
83
- */
84
- readonly transactions: Record<string, TransactionSpec>;
85
- }
86
- /**
87
- * The derived-effect family of one serialized prepared-write transaction item
88
- * (issue #208). Index-aligned to `TransactionSpec.items`; the AOT loader groups
89
- * rendered items by category ACROSS the plan's ops so the composed
90
- * `TransactWriteItems` matches the in-process execution core's order
91
- * (writes, then all edges, then all derived updates, …) byte-for-byte.
92
- */
93
- type PreparedWriteItemCategory = 'base' | 'edge' | 'derive' | 'unique' | 'outbox' | 'idempotency' | 'maintain' | 'maintainOutbox' | 'check';
94
- /**
95
- * Serialize a map of resolved contracts into the `contracts` SSoT map plus the
96
- * synthesized operation specs each method references. Deterministic: contracts
97
- * and methods are emitted in sorted-key order.
98
- *
99
- * @param contracts Contract name → resolved {@link QueryModelContract} /
100
- * {@link CommandModelContract} (the #58 factory output). Empty → empty output.
101
- * @throws if a contract method op cannot be serialized (a non-boolean projection
102
- * slot, an unsupported write condition); or if a contract violates an N+1
103
- * safety rule (#60, {@link assertContractN1Safe}) — an array into a `range`
104
- * method, a "list under a list", or a `range` composed child.
105
- */
106
- declare function buildContracts(contracts?: ContractMap): BuiltContracts;
107
- /**
108
- * Serialize the context-ownership declarations into the SSoT `contexts` map.
109
- * Member lists are sorted for determinism. An empty input yields an empty map
110
- * (the caller then omits the key for backward compatibility).
111
- */
112
- declare function buildContexts(contexts?: ContextOwnershipMap): Record<string, ContextSpec>;
113
-
114
- /**
115
- * Static prepared-plan artifact — the SHARED, dependency-minimal half of the
116
- * AOT pipeline (issue #208; layering fixed by #223).
117
- *
118
- * This module carries everything BOTH sides of the pipeline need:
119
- *
120
- * - the artifact shapes ({@link PreparedPlanDocument} and friends),
121
- * - the format version ({@link PREPARED_FORMAT_VERSION}),
122
- * - canonical JSON ({@link canonicalJson}) and the drift-detection
123
- * fingerprints ({@link entityFingerprint} / {@link planFingerprint}).
124
- *
125
- * ## Layering (#223)
126
- *
127
- * The dependency direction is strictly one-way:
128
- *
129
- * ```
130
- * emitter (src/spec/prepared.ts, build-time) ──▶ THIS MODULE
131
- * loader (src/runtime/prepared-loader.ts) ──▶ THIS MODULE
132
- * ```
133
- *
134
- * The loader must NEVER import the emitter: the emitter drags the whole
135
- * build-time spec machinery into the runtime bundle (`dist/index.js`), which
136
- * is exactly the leak the size-limit CI gate caught in #223. This module's
137
- * only value-level dependencies are ones the loader already carries
138
- * (`buildManifestEntity` / spec types).
139
- *
140
- * ## Fingerprints are checksums, not cryptography
141
- *
142
- * The fingerprint exists to detect DRIFT (model declarations changed since
143
- * the artifact was generated). It is not a security boundary, so it uses a
144
- * pure-TypeScript FNV-1a 64-bit hash — no `node:crypto`, no platform builtin,
145
- * bundles everywhere. Switching the algorithm (sha-256 → FNV-1a) is what
146
- * bumped {@link PREPARED_FORMAT_VERSION} '1' → '2': a format-'1' artifact's
147
- * fingerprints can never match, so the loader rejects the version loudly
148
- * instead of failing the comparison confusingly.
149
- */
150
-
151
- /**
152
- * The prepared-plan artifact format version (independent of {@link SPEC_VERSION}).
153
- *
154
- * '2' (#223): fingerprints are FNV-1a 64-bit over canonical JSON (16 hex
155
- * chars). '1' (#208, graphddb ≤ 0.7.2) used sha-256 via `node:crypto`; the
156
- * loader rejects '1' artifacts with a regeneration instruction.
157
- */
158
- declare const PREPARED_FORMAT_VERSION: "2";
159
- /**
160
- * One serialized value bind: a `$`-param slot (bound from the caller's
161
- * `execute(params)` by name) or a static literal recorded at build time. `Date`
162
- * and `bigint` literals — which JSON cannot carry natively — are encoded
163
- * explicitly and revived by the loader, so the bound value is the SAME typed
164
- * value the runtime `prepare` would have captured.
165
- */
166
- type PreparedBindSpec = {
167
- readonly param: string;
168
- } | {
169
- readonly literal: string | number | boolean | null;
170
- } | {
171
- readonly literalDate: string;
172
- } | {
173
- readonly literalBigInt: string;
174
- };
175
- /** A `{ field: bind }` map (a serialized slot map). */
176
- type PreparedBindMap = Readonly<Record<string, PreparedBindSpec>>;
177
- /** One serialized read route of a prepared READ plan. */
178
- interface PreparedReadRouteSpec {
179
- readonly alias: string;
180
- readonly op: 'query' | 'list';
181
- /** The target entity name (re-bound via the registry at load). */
182
- readonly entity: string;
183
- /** The static, params-independent select template (JSON-safe by the #206 lint). */
184
- readonly select: Record<string, unknown>;
185
- /** Key-field binds (`{ field: { param | literal } }`). */
186
- readonly key: PreparedBindMap;
187
- /** Static option literals decided at build time. */
188
- readonly maxDepth?: number;
189
- readonly order?: 'ASC' | 'DESC';
190
- readonly filter?: Record<string, unknown>;
191
- /** Dynamic option binds (a `$` param or a literal, decided per call). */
192
- readonly consistentRead?: PreparedBindSpec;
193
- readonly limit?: PreparedBindSpec;
194
- readonly after?: PreparedBindSpec;
195
- /**
196
- * The spec-IR read plan (`operations.json` vocabulary: physical ops, templated
197
- * `{<field>}` key conditions, relation fan-outs, execution plan) derived from
198
- * this route at build time. Key-condition tokens are the entity's key FIELD
199
- * names; the per-call param→field mapping is {@link key}. Deriving it at build
200
- * time surfaces a broken route as a BUILD failure and freezes the physical
201
- * plan for drift comparison.
202
- */
203
- readonly query: QuerySpec;
204
- }
205
- /** One serialized write op of a prepared WRITE plan. */
206
- interface PreparedWriteOpSpec {
207
- readonly alias: string;
208
- readonly entity: string;
209
- readonly intent: 'create' | 'update' | 'remove';
210
- /** The target model's primary-key input fields (W1 key/changes split + read-back key). */
211
- readonly keyFields: readonly string[];
212
- /** Key-field binds. */
213
- readonly key: PreparedBindMap;
214
- /** Input-field binds. */
215
- readonly input: PreparedBindMap;
216
- /** In-process write-gate binds (flat equality / existence template), if declared. */
217
- readonly condition?: PreparedBindMap;
218
- /** The read-back projection + options, verbatim (JSON-safe by the #206 lint). */
219
- readonly result?: {
220
- readonly select?: Record<string, unknown>;
221
- readonly options?: {
222
- readonly consistentRead?: boolean;
223
- readonly maxDepth?: number;
224
- };
225
- };
226
- /**
227
- * The consistent read-back's spec-IR plan (derived from `result.select` at
228
- * build time — a broken read-back projection fails the BUILD). Present iff
229
- * {@link result} declares a non-empty select.
230
- */
231
- readonly readback?: QuerySpec;
232
- /**
233
- * The op's full declarative {@link TransactionSpec}: `items[0]` is the base
234
- * write, followed by every derived effect the mutation compiler resolved at
235
- * build time. Templates bind `{<field>}` tokens — the op's key/input FIELD
236
- * names — which the loader renders from the per-call bound field values.
237
- */
238
- readonly transaction: TransactionSpec;
239
- /** Per-item derived-effect categories, index-aligned to `transaction.items`. */
240
- readonly categories: readonly PreparedWriteItemCategory[];
241
- }
242
- /** One compiled prepared plan (all routes of one `DDBModel.prepare` body). */
243
- interface PreparedPlanSpec {
244
- readonly kind: 'read' | 'write';
245
- /**
246
- * The `$` params the plan binds (union across routes), with kinds recovered
247
- * from the bound entity fields where determinable.
248
- */
249
- readonly params: Readonly<Record<string, ParamSpec>>;
250
- /**
251
- * Entity name → fingerprint of its canonical manifest entry at build time.
252
- * The loader recomputes these from the live registry and LOUDLY rejects a
253
- * mismatch (stale plan / model drift).
254
- */
255
- readonly entities: Readonly<Record<string, string>>;
256
- /** Read routes, in body declaration order (read plans only). */
257
- readonly reads?: readonly PreparedReadRouteSpec[];
258
- /** Write ops, in body declaration order (write plans only). */
259
- readonly writes?: readonly PreparedWriteOpSpec[];
260
- }
261
- /** The full static prepared-plan artifact. */
262
- interface PreparedPlanDocument {
263
- readonly formatVersion: typeof PREPARED_FORMAT_VERSION;
264
- /** The shared operation-IR version the plans were compiled against. */
265
- readonly specVersion: typeof SPEC_VERSION;
266
- /** Plan id (the transform's stable call-site id) → compiled plan. */
267
- readonly plans: Readonly<Record<string, PreparedPlanSpec>>;
268
- }
269
- /** Deterministic JSON: object keys sorted at every level (arrays keep order). */
270
- declare function canonicalJson(value: unknown): string;
271
- /**
272
- * The fingerprint of one entity's canonical manifest entry — every plan-relevant
273
- * declaration fact (table, physical name, prefix, fields + formats, key / GSI
274
- * templates, relations incl. `refs` bindings, TTL). Computed identically at
275
- * build (emit) and at load (live registry), so any drift is caught.
276
- */
277
- declare function entityFingerprint(metadata: EntityMetadata): string;
278
- /** Stable content hash of a compiled plan (drift-check convenience). */
279
- declare function planFingerprint(plan: PreparedPlanSpec): string;
280
-
281
- /**
282
- * Declarative transaction definition DSL (issue #46, Python-bridge Phase 4).
283
- *
284
- * `defineTransaction(params, (tx, p) => { … })` captures a **single source of
285
- * truth** for a `TransactWriteItems` batch: a parameter map plus a declarative
286
- * instruction list (`tx.put` / `tx.update` / `tx.delete`, plus `tx.forEach` over
287
- * an array param). The callback is evaluated at definition time with sentinel
288
- * proxies in place of params / loop elements, so the recorded instructions carry
289
- * **field-reference templates** (`{groupId}`, `{item.userId}`) rather than
290
- * concrete values — exactly the shape the static planner (#42) turns into a
291
- * serializable `transaction` op the TS and Python runtimes expand identically.
292
- *
293
- * ## Declarativity boundary (enforced — same soundness as #42 keys)
294
- *
295
- * The callback body may only:
296
- * - call `tx.put/update/delete(Model, …)` with **field references** (from `p`
297
- * or a `forEach` element) or **concrete literals** at scalar leaves;
298
- * - call `tx.forEach(p.<arrayParam>, (el) => { … }, { when? })` whose body is
299
- * itself only `tx.*` writes;
300
- * - attach a declarative `when` (a small comparison over field refs) and/or a
301
- * `condition` (the `{ notExists } | equality` subset, as for single writes).
302
- *
303
- * The same hardening that protects **key** positions (#42 `src/spec/symbolic.ts`)
304
- * is applied to **value** positions (item / changes / condition values, `when`
305
- * right-hand side). Value positions are in fact held to a *stricter* boundary
306
- * than keys: a key may legitimately interpolate a field (`` `USER#${id}` ``), so
307
- * coercion is allowed there and the differential template check is the boundary;
308
- * a value leaf may **only** be a *direct* field reference or a concrete literal,
309
- * so at a value position a faithful reference is **never coerced**. That single
310
- * fact is the soundness boundary:
311
- *
312
- * 1. **Throwing sentinel Proxy.** Each param / element placeholder is a Proxy
313
- * whose `get` trap whitelists only primitive coercion
314
- * (`Symbol.toPrimitive` / `toString` / `valueOf`) and the brand / `token` /
315
- * `origin` reads the planner needs, and **throws on every other property or
316
- * method access**. So a *direct* transform on a value placeholder —
317
- * `p.role.length`, `p.role[0]`, `p.role.toUpperCase()` — fails at the access
318
- * site, exactly like a key sentinel. (Same first-line defense as #42.)
319
- * 2. **Per-access coercion ledger (primary boundary).** Every minted field ref
320
- * records each time it is coerced to a primitive (`Symbol.toPrimitive` /
321
- * `toString` / `valueOf`). Because a faithful value reference is stored as
322
- * the Proxy *object* and never coerced, **any** coercion of a value ref is
323
- * positive proof the callback consumed the parameter into a transform, a
324
- * branch, an interpolation, or a comparison — and the build is rejected. This
325
- * is *access-site* accurate: it fires even when the same field is *also*
326
- * referenced faithfully elsewhere in the item (so its token still surfaces),
327
- * and it catches transforms that **converge to a constant or a shared marker
328
- * fragment** — `String(p.role).slice(0,0)` → `''`, `.slice(0,3)` → a shared
329
- * prefix, `[0]` / `.charAt(0)`, `.includes('Tx')` → a constant boolean, any
330
- * `… + 'CONST'` built atop them — which a purely output-comparing check could
331
- * not (the dropped field is masked by its faithful sibling, and the converged
332
- * value is byte-identical across passes).
333
- * 3. **Differential evaluation (secondary).** The callback is evaluated **twice**,
334
- * each run delivering placeholders that coerce to a *different* per-field
335
- * marker (the two markers share no common prefix and no common substring of
336
- * length ≥ 2, and differ in length). Every recorded value leaf must be either
337
- * (a) a faithful field reference (the Proxy object survived intact in *both*
338
- * runs) or (b) a concrete literal byte-identical across both runs. This is a
339
- * defense-in-depth layer behind the coercion ledger.
340
- * 4. **Consumed-field check.** A placeholder field that the callback *accessed*
341
- * but that never reached the recorded output — e.g. `(p.role === 'admin') ?
342
- * 'A' : 'B'`, where `===` does **not** coerce (so the ledger does not fire),
343
- * the comparison is `false`, and only a literal is emitted — is rejected: the
344
- * param was silently dropped and a branch burned into the spec. (Same
345
- * consumed-field accounting #42 uses for `kind === 'admin' ? … : …`.)
346
- *
347
- * Arbitrary JS, reads, value branching, arithmetic, coercion, or string
348
- * transforms on a param / element placeholder are therefore rejected at build
349
- * time, in **both** key and value positions — **never silently emitted as a
350
- * wrong spec**. Unlike key positions, value positions have **no residual class**:
351
- * because a faithful value ref is never coerced, even an identity-shaped
352
- * transform (`String(p.x).slice(0)`, `.normalize()`, `.trim()`, `.replace('§',…)`
353
- * on an absent substring) is rejected (it coerced the ref). The only operations
354
- * that *do not* coerce a ref are (a) storing it directly and (b) the brand read;
355
- * (a) is the supported faithful reference and (b) is internal — so the ledger has
356
- * no false positives and no escape.
357
- */
358
-
359
- declare const TX_REF_BRAND: unique symbol;
360
- /**
361
- * A captured reference to a parameter (`{name}`) or a `forEach` element field
362
- * (`{item.<field>}`). Branded so the planner can distinguish a reference from a
363
- * concrete literal, and so a stray reference cannot be silently coerced.
364
- */
365
- interface TransactionRef {
366
- readonly [TX_REF_BRAND]: true;
367
- /** The template token this reference renders to, e.g. `{groupId}`. */
368
- readonly token: string;
369
- /** Human-readable origin (for error messages), e.g. `param 'groupId'`. */
370
- readonly origin: string;
371
- }
372
- /** Runtime guard: is `value` a {@link TransactionRef}? */
373
- declare function isTransactionRef(value: unknown): value is TransactionRef;
374
- /** A declarative comparison used by `when` (the only branching allowed). */
375
- type WhenComparison = {
376
- readonly op: 'eq';
377
- readonly left: TransactionRef;
378
- readonly right: unknown;
379
- } | {
380
- readonly op: 'ne';
381
- readonly left: TransactionRef;
382
- readonly right: unknown;
383
- };
384
- /**
385
- * A single captured instruction inside a transaction. The three write operations
386
- * (`put` / `update` / `delete`) mutate an item; `conditionCheck` (issue #81) is a
387
- * **read-only assertion** on another keyed item — it carries a `key` and a
388
- * **required** `condition` and never mutates, but its failure rolls the whole
389
- * transaction back. It is the foundation for referential-integrity derivation
390
- * (`requires <Entity> exists` → a `conditionCheck` with `attributeExists`).
391
- */
392
- interface TxWriteInstruction {
393
- readonly kind: 'write';
394
- readonly operation: 'put' | 'update' | 'delete' | 'conditionCheck';
395
- readonly entity: EntityRef;
396
- /** Put: the item structure (field → ref / literal). */
397
- readonly item?: Record<string, unknown>;
398
- /** Update / delete / conditionCheck: the key structure. */
399
- readonly key?: Record<string, unknown>;
400
- /** Update: the changes structure. */
401
- readonly changes?: Record<string, unknown>;
402
- /**
403
- * The write / assertion condition (subset). Optional on `put` / `update` /
404
- * `delete`; **required** on a `conditionCheck` (the assertion it makes).
405
- *
406
- * A tx condition operand may be a `p.*` field reference ({@link
407
- * TransactionRef}) — the serializer renders it to a `{ $param }` marker — so
408
- * the recorded leaf type is widened with `TransactionRef` in the tx context
409
- * (#246), matching what the recorder captures from {@link TxWriteOptions}.
410
- */
411
- readonly condition?: ConditionInput<TransactionRef>;
412
- /** Optional declarative guard (skip this item when it does not hold). */
413
- readonly when?: WhenComparison;
414
- /**
415
- * Optional SCP expression guard (issue #261, Phase 3 S1): a behavior-contracts
416
- * Expression IR `{ exprVersion: 1, expr }` envelope, carried beside the legacy
417
- * `when`. Validated + canonicalized by the spec serializer; a bundle carrying
418
- * one is stamped spec version 1.2 and loud-rejected by runtimes whose
419
- * expression evaluation has not landed yet (fail-closed).
420
- */
421
- readonly guard?: ExpressionSpec;
422
- }
423
- /** A `forEach` block expanding `body` once per element of an array param. */
424
- interface TxForEachInstruction {
425
- readonly kind: 'forEach';
426
- /** The array-param name iterated. */
427
- readonly source: string;
428
- /** Optional per-element declarative guard. */
429
- readonly when?: WhenComparison;
430
- /** The (write-only) instructions emitted per element. */
431
- readonly body: TxWriteInstruction[];
432
- }
433
- type TxInstruction = TxWriteInstruction | TxForEachInstruction;
434
- /** Options accepted by `tx.put/update/delete`. */
435
- interface TxWriteOptions {
436
- readonly condition?: ConditionInput<TransactionRef>;
437
- readonly when?: WhenComparison;
438
- /** Optional SCP expression guard (issue #261); see {@link TxWriteInstruction.guard}. */
439
- readonly guard?: ExpressionSpec;
440
- }
441
- /**
442
- * Options accepted by `tx.conditionCheck` (issue #81). The `condition` is the
443
- * read-only assertion the check makes against the keyed item and is **required**
444
- * — a ConditionCheck with no condition is meaningless. An optional `when` skips
445
- * the check entirely when the guard does not hold.
446
- */
447
- interface TxConditionCheckOptions {
448
- readonly condition: ConditionInput<TransactionRef>;
449
- readonly when?: WhenComparison;
450
- /** Optional SCP expression guard (issue #261); see {@link TxWriteInstruction.guard}. */
451
- readonly guard?: ExpressionSpec;
452
- }
453
- /** Options accepted by `tx.forEach`. */
454
- interface TxForEachOptions {
455
- readonly when?: WhenComparison;
456
- }
457
- /** A small comparison helper: `when.eq(ref, value)` / `when.ne(ref, value)`. */
458
- declare const when: {
459
- readonly eq: (left: TransactionRef, right: unknown) => WhenComparison;
460
- readonly ne: (left: TransactionRef, right: unknown) => WhenComparison;
461
- };
462
- /** The transaction recorder handed to the callback as `tx`. */
463
- interface TxRecorder {
464
- put(model: ModelStatic<DDBModel>, item: Record<string, unknown>, options?: TxWriteOptions): void;
465
- update(model: ModelStatic<DDBModel>, key: Record<string, unknown>, changes: Record<string, unknown>, options?: TxWriteOptions): void;
466
- delete(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options?: TxWriteOptions): void;
467
- /**
468
- * Record a **read-only** `ConditionCheck` assertion on a keyed item (issue
469
- * #81): the item is not mutated, but `options.condition` must hold or the whole
470
- * transaction is cancelled atomically. The foundation for referential-integrity
471
- * derivation (`requires <Entity> exists` → `conditionCheck(Model, key, {
472
- * condition: { attributeExists: 'PK' } })`).
473
- */
474
- conditionCheck(model: ModelStatic<DDBModel>, key: Record<string, unknown>, options: TxConditionCheckOptions): void;
475
- forEach<E>(source: E, body: (element: ElementProxy<E>) => void, options?: TxForEachOptions): void;
476
- }
477
- /** The element proxy type inside a `forEach` body (fields → refs). The second
478
- * arm types a **value-typed** array source — the shape the SCP native-syntax
479
- * form's `p` proxy carries (issue #263: `defineScpTransaction`'s transformed
480
- * output calls `tx.forEach(p.<arrayParam>, …)` through the value-typed view);
481
- * at runtime the source is always the array param placeholder either way. */
482
- type ElementProxy<E> = E extends Param<infer T> ? T extends (infer Item)[] ? {
483
- readonly [K in keyof Item]: TransactionRef;
484
- } : never : E extends readonly (infer Item)[] ? {
485
- readonly [K in keyof Item]: TransactionRef;
486
- } : never;
487
- /** The typed IR node produced by {@link defineTransaction}. */
488
- interface TransactionDefinition {
489
- /** @internal Marks this object as a transaction definition IR node. */
490
- readonly __isTransactionDefinition: true;
491
- /** Collected parameters: name → descriptor (array params carry `element`). */
492
- readonly params: Readonly<Record<string, ParamDescriptor>>;
493
- /** The captured, declarative instruction list. */
494
- readonly instructions: readonly TxInstruction[];
495
- }
496
- /** A param map accepted by {@link defineTransaction}. */
497
- type TransactionParamShape = Record<string, Param<unknown>>;
498
- /**
499
- * The `p` proxy type: each scalar param → ref; each array param → itself.
500
- *
501
- * ## Input Port 統一 (issue #264, Phase 3 S4; the #252 note)
502
- *
503
- * This `p.*` proxy **IS the Input Port model**: the three existing authoring
504
- * proxies — this one, `mutation`'s `$.*`
505
- * ({@link import('./mutation.js').MutationInputProxy}) and `prepare`'s `$.*`
506
- * ({@link import('../runtime/prepared.js').PreparedInputProxy}) — share one
507
- * reference semantics: *a declared input field, referenced by name, bound at
508
- * execution time*. The serialized spellings mirror each other 1:1:
509
- *
510
- * - template positions render `{<field>}` / `{item.<field>}` (the `{param}` /
511
- * `{item.*}` scopes of behavior-contracts runtime-boundary.md §4.1);
512
- * - lowered EXPRESSION positions (guards — issues #262/#263/#264) reference
513
- * the same namespace as `{ref:["input","<field>"]}` /
514
- * `{ref:["item","<field>"]}` with the FIXED scope names `input` / `item`.
515
- *
516
- * The namespace is single: expression refs must name declared `param.*`
517
- * inputs (enforced at build by `spec/transaction.ts` and at lowering by the
518
- * declared-field gate — the #243 no-unbound-leak rule). `param.*` authoring
519
- * itself is UNCHANGED through 0.7.x (interop kept; bundles byte-identical) —
520
- * its absorption into the unified surface is the #251/#252 0.8.0 work.
521
- */
522
- type ParamProxy<P extends TransactionParamShape> = {
523
- readonly [K in keyof P]: P[K] extends {
524
- readonly kind: 'array';
525
- } ? P[K] : TransactionRef;
526
- };
527
- /**
528
- * Define a declarative transaction. The callback is evaluated **twice** with
529
- * throwing value sentinels (one differential marker set per pass); its `tx.*`
530
- * calls are recorded and the two passes are checked against each other so any
531
- * non-declarative value (a transform, a value branch, a coercion escape) is
532
- * rejected at build time — the same soundness #42 enforces for key positions.
533
- *
534
- * @param params A record of `param.*` placeholders (scalars and/or `param.array`).
535
- * @param build `(tx, p) => void` — records the write instructions.
536
- */
537
- declare function defineTransaction<const P extends TransactionParamShape>(params: P, build: (tx: TxRecorder, p: ParamProxy<P>) => void): TransactionDefinition;
538
- /**
539
- * The **value-typed** params view of the SCP native-syntax body form: each
540
- * scalar param surfaces as its represented value type (`p.qty: number`,
541
- * `p.status: 'open' | 'closed'`) and each array param as its element-object
542
- * array (`p.items: { qty: number }[]`), so native conditions
543
- * (`p.qty > 0 && tx.put(…)`) and `.map` fan-out typecheck as ordinary TS.
544
- */
545
- type ScpParamValues<P extends TransactionParamShape> = {
546
- readonly [K in keyof P]: P[K] extends Param<infer T> ? T : never;
547
- };
548
- /**
549
- * The lowered-body marker `transformScpTransactionSource` stamps as the third
550
- * argument of every `defineScpTransaction` call it compiles (issue #263).
551
- *
552
- * {@link defineScpTransaction} **throws at definition time when the marker is
553
- * absent** — this is the mechanical no-silent-path closure. The recorder's
554
- * own guards cannot close every untransformed native shape: `===` / `!==` do
555
- * not coerce (the coercion ledger never fires), ToBoolean has no Proxy trap
556
- * (bare truthiness is invisible), and the consumed-field check is
557
- * token-level, so a param consumed by a native condition is masked whenever
558
- * the same field ALSO surfaces faithfully in another item — an untransformed
559
- * body could then silently record a wrong spec (a dropped or unguarded
560
- * item). The marker check does not depend on any of those blind spots: an
561
- * untransformed `defineScpTransaction` never records anything. The ledger /
562
- * consumed-field / differential checks remain as defense-in-depth behind it.
563
- * Hand-writing the marker is a deliberate, greppable opt-out, not a silent
564
- * path.
565
- */
566
- declare const SCP_LOWERED_MARKER: "graphddb:scp-lowered/v1";
567
- /**
568
- * Define a declarative transaction in the **SCP native-syntax body form**
569
- * (issue #263, Phase 3 S3): the body may additionally use TS native control
570
- * syntax — `cond && tx.put(…)` (Guard), `cond ? tx.put(A,…) : tx.put(B,…)`
571
- * (Conditional / Φ), and `p.<arrayParam>.map(($el) => tx.put(…))` (Map) —
572
- * which the **opt-in build-time transform**
573
- * (`transformScpTransactionSource`, `graphddb/transform`) lowers onto the
574
- * untouched recorder vocabulary: `guard:` options (S2-lowered Expression IR
575
- * conditions) and `tx.forEach`. The `p` proxy is value-typed
576
- * ({@link ScpParamValues}) so the native form typechecks.
577
- *
578
- * The transform stamps every call it compiles with
579
- * {@link SCP_LOWERED_MARKER}; an unstamped (= untransformed) call **throws
580
- * here at definition time**, so a native-operator body can never record a
581
- * spec without having been lowered (see the marker doc for why the
582
- * recorder's own guards alone cannot close the masked `===` / `!==` /
583
- * truthiness shapes). A stamped call **delegates directly to
584
- * {@link defineTransaction}** — the transformed output is ordinary recorder
585
- * authoring, still covered by the coercion ledger / differential passes as
586
- * defense-in-depth.
587
- */
588
- declare function defineScpTransaction<const P extends TransactionParamShape>(params: P, build: (tx: TxRecorder, p: ScpParamValues<P>) => void, lowered?: typeof SCP_LOWERED_MARKER): TransactionDefinition;
589
- /**
590
- * Group a record of transaction definitions into the IR consumed by the static
591
- * planner (#42/#46). Validates each entry is a transaction definition.
592
- */
593
- declare function defineTransactions<const D extends Record<string, TransactionDefinition>>(definitions: D): D;
594
-
595
- /**
596
- * Static parameterized operation-spec generation (issue #42, Phase 0b).
597
- *
598
- * Turns the parameterized definition IR (#41) into JSON-serializable
599
- * {@link OperationSpec} / {@link CommandSpec} via **symbolic evaluation** of the
600
- * key / GSI mapping functions. The result is deterministic and contains only
601
- * `{param}` / `{result.field}` template placeholders, never concrete values.
602
- *
603
- * Relation chains in a read `select` become **multiple operations** wired with
604
- * `resultPath` and `{result.*}` key templates, mirroring the runtime
605
- * `relation-planner` policy: `hasMany` → `Query` + `begins_with`, `belongsTo` /
606
- * `hasOne` → `BatchGetItem`.
607
- */
608
-
609
- /** Build the {@link QuerySpec} for a single read definition. */
610
- declare function buildQuerySpec(def: AnyOperationDefinition): QuerySpec;
611
- /**
612
- * Optional contract-layer inputs to {@link buildOperations} (issue #59). Both are
613
- * absent on a pre-#59 (contract-free) build, in which case the produced document
614
- * is byte-identical to the pre-#59 shape (no `contracts` / `contexts` keys).
615
- */
616
- interface ContractInputs {
617
- /** Resolved contracts (publicQueryModel / publicCommandModel output). */
618
- readonly contracts?: ContractMap;
619
- /** Context-ownership declarations (context → member models / contracts). */
620
- readonly contexts?: ContextOwnershipMap;
621
- }
622
- /**
623
- * Build the operations document from grouped read (`queries`) and write
624
- * (`commands`) definition maps, plus the optional CQRS contract layer (#59).
625
- * All inputs are optional. Output is deterministic (definitions emitted in
626
- * sorted-key order; the runtime resolves by name).
627
- *
628
- * The contract layer (#59) is layered **on top of** the existing specs: each
629
- * contract method's underlying op is synthesized into `queries` / `commands` and
630
- * referenced by name, and the `contracts` / `contexts` maps are emitted. A
631
- * contract-free input (`contractInputs` empty / absent) reproduces the pre-#59
632
- * document byte-for-byte — no `contracts` / `contexts` keys appear.
633
- *
634
- * @throws if a command carries a write condition outside the supported subset
635
- * (`{ notExists }` or pure equality) — the bridge guard; or if a contract op
636
- * name collides with a hand-written definition.
637
- */
638
- declare function buildOperations(queries?: DefinitionMap, commands?: DefinitionMap, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): OperationsDocument;
639
-
640
- /**
641
- * Manifest generation (issue #42, Python-bridge Phase 0b).
642
- *
643
- * Produces the JSON-serializable {@link Manifest} from a {@link MetadataRegistry}
644
- * snapshot: entity → table / physicalName / prefix / field types / key & GSI
645
- * templates / relation metadata. Generation is **deterministic** — entities,
646
- * fields, GSIs, and relations are emitted in a stable (sorted) order so the same
647
- * registry always yields byte-identical JSON.
648
- */
649
-
650
- /**
651
- * Build the full {@link Manifest} from the {@link MetadataRegistry}.
652
- *
653
- * Every registered entity is included. Output ordering is deterministic
654
- * (entities / fields / relations sorted by name, GSIs by index name).
655
- */
656
- declare function buildManifest(registry?: typeof MetadataRegistry): Manifest;
657
-
658
- /**
659
- * N+1 static checker for the CQRS Contract layer (issue #60, Epic #57; proposal
660
- * `docs/cqrs-contract.md`, "N+1 Safety (Static Restriction)").
661
- *
662
- * Whether a resolution is N+1-safe depends on **two** facts the planner already
663
- * decided in #58 and #59 serialized into the SSoT — the checker **honors** them,
664
- * it does **not** re-derive policy ("decide once, declare in the SSoT"):
665
- *
666
- * - `resolution` — `'point'` (target keys known → `BatchGetItem` / another
667
- * contract's `get(keys[])`; coalesces) or `'range'` (target key set unknown →
668
- * partition `Query`; one request per partition key).
669
- * - `inputArity` / parent `cardinality` — how many keys are fed in. A `range`
670
- * method is one query for one key but N queries for N keys.
671
- *
672
- * The rule (absolute, no opt-in escape hatch): **a `range` resolution is allowed
673
- * only with single-key input.** Three forms are build-time errors:
674
- *
675
- * (a) **array into a `range` method** — a `range` method whose `inputArity` is
676
- * not `'single'` (`'array'` / `'either'`). A `range` method must be
677
- * `'single'`; anything else is an N partition-`Query` fan-out.
678
- * (a′) **array into a unique-GSI `point` method** (issue #71) — a `point` method
679
- * whose Key resolves via a **unique GSI** (a per-key `Query`, since
680
- * `BatchGetItem` cannot read a GSI) but whose `inputArity` is not `'single'`.
681
- * A base-table `point` coalesces a key array into one `BatchGetItem`, but a
682
- * unique-GSI `point` would issue N `Query`s — an N+1 fan-out, the same shape
683
- * as (a). graphddb's official spec does not include N+1-producing queries, so
684
- * it is rejected at build time; the application-side loop is the escape (same
685
- * philosophy as `range`).
686
- * (b) **"list under a list"** — a `range` child nested under a parent step whose
687
- * output cardinality is `'many'`. The composition's parent yields N records,
688
- * so a `range` child fans out to N partition `Query`s.
689
- * (c) **`compose` child that is `range`** — the cross-contract / composition form
690
- * of (b): a composed (External Query) child declared `range` under a parent
691
- * that can yield many. (Rule (b) and (c) are the same axis — a `range` child
692
- * under a many-yielding parent — surfaced through nested model relations vs.
693
- * cross-contract composition respectively.)
694
- *
695
- * `point` resolutions are always allowed (they coalesce). If a caller genuinely
696
- * needs "just these few", it loops in its own application code (the proposal's
697
- * `Promise.all(ids.map(...))`), keeping the N visible at the call site —
698
- * the contract surface never hides a fan-out.
699
- *
700
- * ## Placement — build path, not the model `LintRule` framework
701
- *
702
- * This checker runs on the **contract build path** ({@link buildContracts} in
703
- * `src/spec/contracts.ts`), where every other contract validation surfaces
704
- * (#59's projection audit, write-condition guard, and the original inline
705
- * `compose` rule all `throw` there). It operates on the serialized
706
- * {@link ContractSpec} facts, not on {@link EntityMetadata}: the existing
707
- * `src/linter/` `LintRule` framework is keyed on a single model's `EntityMetadata`
708
- * (model relations / GSIs), which carries none of the contract `resolution` /
709
- * `inputArity` / `compose` facts the N+1 rule needs. Surfacing the violation as a
710
- * thrown build error — exactly as `relation/traversal.ts` `validateDepth` and the
711
- * #59 contract validations do — fails the build the same way `inputArity`
712
- * mismatches and `range` composed children already failed it.
713
- *
714
- * The cross-contract `range`-child form (c) and the nested-relation form (b) are
715
- * only fully reachable once the composition DSL primitive
716
- * (`query(Contract.method, { from() })`, owned by #63) lands; until then no #58
717
- * DSL produces a `compose` node. The checker implements the full logic now so it
718
- * is ready, and the forward forms are exercised by feeding synthesized SSoT nodes
719
- * (exactly as #59's serializer tests do).
720
- */
721
-
722
- /** A single N+1 rule violation, carrying the rule label and a clear message. */
723
- interface ContractN1Violation {
724
- /** Which of the N+1 forms was violated. */
725
- readonly rule: 'array-into-range' | 'array-into-gsi-point' | 'list-under-list' | 'compose-range-under-many';
726
- /** The offending contract name. */
727
- readonly contract: string;
728
- /** The offending method name. */
729
- readonly method: string;
730
- /** A clear, actionable error message (the same text the thrown error carries). */
731
- readonly message: string;
732
- }
733
- /**
734
- * Tells, for a query contract method name, whether its `point` read resolves via
735
- * a **unique GSI** (a per-key `Query`) rather than the base-table primary key (a
736
- * coalescible `GetItem` / `BatchGetItem`). Supplied by the serializer (issue #71),
737
- * which recovers the fact from the method's synthesized read op (a root `Query`
738
- * carrying an `indexName`). Absent → no method is treated as a GSI point (the
739
- * pre-#71 default; e.g. synthesized-SSoT checker tests that exercise only the
740
- * `range` / `compose` rules).
741
- */
742
- type IsGsiPointMethod = (methodName: string) => boolean;
743
- /**
744
- * Collect every N+1 violation in a single serialized {@link ContractSpec}.
745
- * Command contracts carry no `resolution` / `compose` and are always N+1-safe
746
- * (writes resolve to known keys), so only query contracts are inspected.
747
- *
748
- * @param isGsiPoint Optional predicate (issue #71): does the named method's
749
- * `point` read resolve via a unique GSI? Used to reject array-into-GSI-point
750
- * (rule a′). Defaults to "never" when not supplied.
751
- */
752
- declare function collectContractN1Violations(contractName: string, spec: ContractSpec, isGsiPoint?: IsGsiPointMethod): ContractN1Violation[];
753
- /**
754
- * Assert a single serialized contract is N+1-safe, throwing the first violation
755
- * as a build error (mirroring `relation/traversal.ts` `validateDepth` and the
756
- * #59 contract validations, which `throw` on the build path). Multiple violations
757
- * are summarized in the thrown message so the build surfaces them all at once.
758
- *
759
- * @throws if any of the N+1 forms is present.
760
- *
761
- * @param isGsiPoint Optional predicate (issue #71) identifying unique-GSI `point`
762
- * methods, so rule (a′) (array into a GSI point) can be enforced.
763
- */
764
- declare function assertContractN1Safe(contractName: string, spec: ContractSpec, isGsiPoint?: IsGsiPointMethod): void;
765
-
766
- /**
767
- * Bounded-context boundary checker for the CQRS Contract layer (issue #61, Epic
768
- * #57; spec `docs/cqrs-contract.md`, External Query section "Why it
769
- * exists: boundary enforcement").
770
- *
771
- * The rule, stated in the proposal and the issue: a **Model is a private
772
- * implementation detail of its bounded context; a Contract is the published
773
- * interface**. Within its own context a contract resolves its own Models directly
774
- * (internal `query` / `list` / `put` / `update` / `delete`). Across a context
775
- * boundary it may depend on another context **only through that context's
776
- * published Contract** (an External Query / Command). Reaching directly into a
777
- * **foreign** Model is a **build-time error** — it couples one context to
778
- * another's private storage shape and silently defeats the boundary the contexts
779
- * declaration is meant to enforce.
780
- *
781
- * ## What "own" vs. "foreign" means here
782
- *
783
- * The decision is made entirely from the **context-ownership declaration** (#59,
784
- * the `contexts` map: context name → `{ models, contracts }`). For each contract:
785
- *
786
- * - its **owning context** is the (unique) context whose `contracts` list names
787
- * it; and
788
- * - for every Model its method ops touch (the recorded {@link ContractMethodOp}
789
- * `entity` refs, #58), that Model's **owning context** is the context whose
790
- * `models` list names it.
791
- *
792
- * A method op against a Model owned by a **different** context than the contract's
793
- * own is the violation. Three cases are deliberately **allowed**, never flagged:
794
- *
795
- * - the Model and the contract are in the **same** context (the normal internal
796
- * case);
797
- * - the **contract is in no declared context** — there is no boundary to enforce
798
- * *from*, so the contract is unconstrained (a partial / incremental adoption of
799
- * the contexts declaration must not turn unrelated contracts into errors);
800
- * - the touched **Model is in no declared context** — an undeclared Model has no
801
- * owner to protect, so it is not "foreign" to anyone. (Declaring the Model into
802
- * a context is what opts it into boundary protection.)
803
- *
804
- * In short: the check fires **only** when both the contract *and* the foreign
805
- * Model have declared, *different* owners. This is the least-surprising reading of
806
- * a *partial* contexts declaration and keeps the rule purely additive — declaring
807
- * more ownership can only ever surface *more* violations, never fewer.
808
- *
809
- * ## Placement — build path, not the model `LintRule` framework (decision B)
810
- *
811
- * This checker runs in **{@link buildOperations}** (`src/spec/operations.ts`),
812
- * immediately before the {@link buildContracts} call — the seam where both the
813
- * raw contract IR (the recorded op `entity` refs) and the `contexts` map are
814
- * still available, alongside #59's projection audit, the write-condition guard,
815
- * and #60's N+1 checker — every contract validation surfaces there as a thrown
816
- * build error. It is **not** added to the `src/linter/` `LintRule`
817
- * framework, even though the issue text suggests a `context-boundary` rule there,
818
- * because that framework's contract — `check(metadata: EntityMetadata, registry)`
819
- * — is keyed on a **single model's** {@link EntityMetadata} and carries **none** of
820
- * the facts this rule needs:
821
- *
822
- * - which context a *contract* belongs to (a contract is not an `EntityMetadata`);
823
- * - which Models a contract's *methods* touch (these live on the unserialized
824
- * {@link ContractMethodOp} `entity` refs, not on any model's metadata);
825
- * - the cross-cutting `contexts` map relating the two.
826
- *
827
- * A `LintRule` sees one model at a time and no contract facts at all, so a naive
828
- * rule physically cannot observe a contract→foreign-Model reference. Bending the
829
- * framework to carry contracts + contexts would be an invasive, parallel
830
- * "rule kind" that no other rule uses — exactly the ugly refactor the brief warns
831
- * against. Placing the check here is consistent with how #60 already resolved the
832
- * identical tension (`src/spec/contract-n1-check.ts`, "Placement"), and it reads
833
- * the **same facts the rule actually depends on** at the one point where contracts
834
- * and contexts are both in hand. The boundary violation fails the build the same
835
- * way an `inputArity` mismatch or a `range` composed child already does.
836
- */
837
-
838
- /** A single context-boundary violation, carrying the offending facts + message. */
839
- interface ContractBoundaryViolation {
840
- /** The offending contract name. */
841
- readonly contract: string;
842
- /** The context that owns the offending contract. */
843
- readonly contractContext: string;
844
- /** The contract method whose op touched the foreign Model. */
845
- readonly method: string;
846
- /** The foreign Model (entity / model-class name) referenced directly. */
847
- readonly foreignModel: string;
848
- /** The context that owns the foreign Model. */
849
- readonly foreignContext: string;
850
- /** A clear, actionable error message (the same text the thrown error carries). */
851
- readonly message: string;
852
- }
853
- /**
854
- * Collect every context-boundary violation across a map of resolved contracts,
855
- * given the context-ownership declaration. A violation is a contract method whose
856
- * recorded op touches a Model owned by a **different** context than the contract's
857
- * own (see the module doc for the precise own / foreign / undeclared rule).
858
- *
859
- * @param contracts Contract name → resolved {@link QueryModelContract} /
860
- * {@link CommandModelContract} (the #58 factory output).
861
- * @param contexts The context-ownership declaration (#59). When empty, no
862
- * contract has a declared owner, so there are no boundaries to enforce → no
863
- * violations.
864
- * @throws if the contexts declaration claims a Model / Contract in two contexts
865
- * (an ambiguous, self-contradictory declaration).
866
- */
867
- declare function collectContractBoundaryViolations(contracts: ContractMap, contexts: ContextOwnershipMap): ContractBoundaryViolation[];
868
- /**
869
- * Assert a map of resolved contracts respects bounded-context boundaries, throwing
870
- * the first violation as a build error (mirroring #60's {@link assertContractN1Safe}
871
- * and the other #59 contract validations, which `throw` on the build path).
872
- * Multiple violations are summarized in the thrown message so the build surfaces
873
- * them all at once.
874
- *
875
- * @param contracts Contract name → resolved contract IR.
876
- * @param contexts The context-ownership declaration (#59).
877
- * @throws if any contract directly references a foreign context's Model, or if the
878
- * contexts declaration is ambiguous (a Model / Contract claimed twice).
879
- */
880
- declare function assertContractBoundaries(contracts: ContractMap, contexts: ContextOwnershipMap): void;
881
-
882
- /**
883
- * Static planning for declarative transactions (issue #46, Phase 4).
884
- *
885
- * Turns a {@link TransactionDefinition} (captured by `defineTransaction`) into a
886
- * serializable {@link TransactionSpec}. Each `tx.put/update/delete` becomes a
887
- * templated {@link TransactionItemSpec}; a `tx.forEach` block marks its items
888
- * with a `forEach` binding so the runtime expands them once per array-param
889
- * element. Key templates are derived with the **same** symbolic key-mapping
890
- * evaluator (#42 `evaluateKey`) the single-write planner uses — the
891
- * declarativity of the field references is what lets that reuse work: every
892
- * key / item / changes leaf is either a field reference (whose token the planner
893
- * substitutes) or a concrete literal.
894
- */
895
-
896
- /** Build the {@link TransactionSpec} for one transaction definition. */
897
- declare function buildTransactionSpec(txName: string, def: TransactionDefinition): TransactionSpec;
898
- /** Build the transactions map from a record of transaction definitions. */
899
- declare function buildTransactions(transactions?: Record<string, TransactionDefinition>): Record<string, TransactionSpec>;
900
-
901
- /**
902
- * Bridge guard (issue #42, Phase 0b).
903
- *
904
- * The Python bridge can only carry a **declarative, serializable subset** of
905
- * graphddb. This module enforces that subset at build time:
906
- *
907
- * 1. **Write conditions** must be `{ notExists }`, an existence primitive
908
- * (`{ attributeExists }` / `{ attributeNotExists }` on any field, issue #81),
909
- * *or* pure field equality (mirroring `expression/condition-expression.ts`,
910
- * which only emits `attribute_(not_)exists(...)` or `#f = :v` clauses). Any
911
- * other condition shape is rejected — there is no serializable representation
912
- * for it. {@link conditionInputToSpec} is the single place the IR-shaped input
913
- * is mapped to the {@link ConditionSpec} subset.
914
- * 2. The produced bundle must be **JSON-serializable** — no functions, classes,
915
- * `Date`, `Set`, `Map`, `Symbol`, or non-finite numbers may leak into the
916
- * spec. {@link assertJsonSerializable} walks the value and throws on any.
917
- *
918
- * (Note: the post-load TS predicate `refine` was removed in #49, so no guard for
919
- * it is needed; see `docs/python-bridge.md`.)
920
- */
921
-
922
- /**
923
- * Assert a write {@link ConditionSpec} is within the supported subset. The spec
924
- * type already constrains it to `notExists` | `attributeExists` |
925
- * `attributeNotExists` | `equals`, so this defends against a malformed `equals`
926
- * (empty / non-template values) or a blank existence field and gives a clear,
927
- * single-source build-time error message for any future widening.
928
- */
929
- declare function assertSupportedCondition(commandName: string, condition: ConditionSpec): void;
930
- /**
931
- * Walk an arbitrary value and throw if it contains anything that does not
932
- * survive `JSON.stringify` round-tripping as itself (functions, symbols,
933
- * `undefined`, `Date`, `Map`, `Set`, class instances, non-finite numbers).
934
- */
935
- declare function assertJsonSerializable(value: unknown, path?: string): void;
936
- /** Convenience: assert an entire {@link BridgeBundle} is JSON-serializable. */
937
- declare function assertBundleSerializable(bundle: BridgeBundle): void;
938
-
939
- /**
940
- * SCP Expression IR — build-time validation + canonicalization (issue #261,
941
- * Phase 3 S1; behavior-contracts `expression-ir.md` §2).
942
- *
943
- * This module is the spec-side **vocabulary** gate: it validates an authored
944
- * expression against the closed v1 node/operator set (fail-closed — anything
945
- * outside the set is a build error, never a silently-carried unknown) and
946
- * rewrites it into the **canonical serialization form** of §2.3/§2.4:
947
- *
948
- * - object keys sorted in **Unicode code-point order** at every level
949
- * (canonical-serialization.md §2 — NOT JS default `.sort()`, which compares
950
- * UTF-16 code units and diverges on astral-plane keys);
951
- * - an integer-valued JSON number within the safe range stays a plain number
952
- * (an int literal); beyond ±2^53−1 it must be `{int:"…"}` (raw is invalid);
953
- * - `{int:"…"}` whose value fits the safe range canonicalizes to the plain
954
- * number; outside it stays `{int:"…"}` (must fit i64);
955
- * - `{float:n}` with an integer value stays wrapped; a fractional `{float:n}`
956
- * canonicalizes to the plain number (JSON preserves its decimal part).
957
- *
958
- * Because the canonical form of the expression NODES is produced HERE (at
959
- * build time), the documents flow through graphddb's ordinary JSON emitters
960
- * unchanged — no special-cased serializer. Note the distinction: §2.3/§2.4
961
- * key-sorting governs the expression nodes (`obj` construction keys; operator
962
- * nodes are single-key by definition); the `{ exprVersion, expr }` ENVELOPE and
963
- * the surrounding spec documents serialize in graphddb's usual emitter order
964
- * like every other spec object (conformance value comparison is structural,
965
- * expression-ir.md §2.4).
966
- *
967
- * Evaluation lives in the Phase 3 S5/S6/S7 sub-issues — this module never
968
- * evaluates anything. It also hosts the conditional spec-version presence scan
969
- * ({@link operationsSpecVersion}): a document that actually CONTAINS an SCP
970
- * node is stamped `1.2`, everything else keeps `1.1` byte-identically.
971
- */
972
-
973
- /**
974
- * Validate an authored `{ exprVersion: 1, expr: <node> }` envelope against the
975
- * closed v1 vocabulary and return it in canonical form (§2.3/§2.4: key-sorted,
976
- * normalized `{int:"…"}` / `{float:n}` literal wrapping). Fail-closed: an
977
- * unknown `exprVersion`, an extra envelope key, or any node outside the closed
978
- * set is a build error with the offending path.
979
- */
980
- declare function canonicalizeExpressionSpec(input: unknown, context: string): ExpressionSpec;
981
- /**
982
- * Does this operations-document content actually contain an SCP Expression IR
983
- * node — a `{ kind: 'scpExpr' }` write condition or a `guard` (transaction
984
- * item / contract command method)? This is the **presence scan** behind the
985
- * conditional stamp (issue #261): the walk visits exactly the slots the spec
986
- * vocabulary defines, so an SCP-free document is provably stamped `1.1` and
987
- * stays byte-identical.
988
- */
989
- declare function operationsContainScpNodes(doc: {
990
- readonly commands: Readonly<Record<string, CommandSpec>>;
991
- readonly transactions?: Readonly<Record<string, TransactionSpec>>;
992
- readonly contracts?: Readonly<Record<string, ContractSpec>>;
993
- }): boolean;
994
- /**
995
- * The spec version to stamp on an operations document: `1.2` iff it actually
996
- * contains an SCP node ({@link operationsContainScpNodes}), else the base
997
- * `1.1` — so every pre-#261 bundle serializes byte-identically.
998
- */
999
- declare function operationsSpecVersion(doc: Parameters<typeof operationsContainScpNodes>[0]): SpecVersion;
1000
-
1001
- /**
1002
- * Static prepared-plan artifact — the **build-time** (AOT) compilation of
1003
- * `DDBModel.prepare` bodies (issue #208; design #203 phase 4, on top of the #205
1004
- * runtime and the #206 compile-time hoist).
1005
- *
1006
- * `graphddb transform prepared --aot` evaluates each no-capture-verified prepare
1007
- * body at BUILD time (the #206 lint proves build-time evaluability) and this
1008
- * module compiles the evaluated routes into a JSON-serializable
1009
- * {@link PreparedPlanDocument}:
1010
- *
1011
- * - a **write** plan runs the SAME mutation compiler the runtime `prepare` uses
1012
- * (`compileWriteFragment` → lifecycle resolution → derived-effect derivation)
1013
- * and serializes the result through the SAME contract-SSoT path a
1014
- * `.plan(mutation)` command method uses
1015
- * ({@link serializePreparedWriteFragment} → one declarative
1016
- * {@link TransactionSpec} per op: base write + edges / counters / uniqueness
1017
- * guards / outbox / idempotency / maintenance / ConditionChecks) — so the
1018
- * runtime never invokes the mutation compiler again, **including the first
1019
- * call** (the #206 audit's cold-start gap);
1020
- * - a **read** plan serializes the analyzed route products (entity name, select
1021
- * template, per-field param binds, static/dynamic options) plus the
1022
- * {@link QuerySpec} the spec planner derives for it — the same
1023
- * `operations.json` vocabulary the Python runtime consumes (templated
1024
- * `{param}` key conditions, physical ops, execution plan), so a broken read
1025
- * plan fails the BUILD, not the first request.
1026
- *
1027
- * ## Model references are entity NAMES
1028
- *
1029
- * The artifact never carries a class: each plan records the entity names it
1030
- * touches plus a **fingerprint** of each entity's canonical manifest entry
1031
- * ({@link entityFingerprint}). The TS loader (`src/runtime/prepared-loader.ts`)
1032
- * re-binds names against the live {@link MetadataRegistry} and recomputes the
1033
- * fingerprints — a mismatch (model declaration / TableMapping drift since the
1034
- * artifact was generated) is a LOUD reject, never a stale-plan execution.
1035
- *
1036
- * Everything emitted is JSON-serializable by construction (the #206 no-capture
1037
- * lint guarantees the body's templates are static data; the only non-JSON
1038
- * scalars a template may carry — `Date` / `bigint` literals — are encoded
1039
- * explicitly in {@link PreparedBindSpec}).
1040
- *
1041
- * ## Layering (#223)
1042
- *
1043
- * This module is BUILD-TIME ONLY. The artifact shapes, the format version, and
1044
- * the fingerprint/canonical-JSON helpers the runtime loader also needs live in
1045
- * the shared, dependency-minimal `./prepared-artifact.ts` — both this emitter
1046
- * and the loader (`src/runtime/prepared-loader.ts`) depend on that module, and
1047
- * the loader NEVER imports this one (a loader→emitter import leaks the whole
1048
- * build-time spec machinery into the runtime bundle — the #223 size-limit
1049
- * failure). The shared symbols are re-exported here for API continuity.
1050
- */
1051
-
1052
- /**
1053
- * Compile ONE `DDBModel.prepare` body into its static {@link PreparedPlanSpec}
1054
- * (build-time). The body is evaluated once with the recording `$` proxy — the
1055
- * #206 no-capture lint has already proven this is side-effect-free and
1056
- * params-independent — then each route is analyzed and serialized.
1057
- *
1058
- * @param body The prepare body function (as extracted by the transform).
1059
- * @param planLabel A human-readable label woven into build errors (the plan id).
1060
- */
1061
- declare function compilePreparedPlan(body: PreparedBody, planLabel?: string): PreparedPlanSpec;
1062
- /** Assemble the full artifact document from compiled plans. */
1063
- declare function buildPreparedPlanDocument(plans: Readonly<Record<string, PreparedPlanSpec>>): PreparedPlanDocument;
1064
-
1065
- /**
1066
- * Static parameterized planner: serializable OperationSpec + manifest + bridge
1067
- * guard (issue #42, Python-bridge Phase 0b). Public surface.
1068
- *
1069
- * The CLI / file output is #43's responsibility; this module stops at the
1070
- * **generation functions** producing the typed, JSON-serializable
1071
- * `{ manifest, operations }` bundle.
1072
- */
1073
-
1074
- /**
1075
- * Build the full `{ manifest, operations }` bridge bundle from grouped query /
1076
- * command definition maps. The bundle is typed and JSON-serializable; the bridge
1077
- * guard runs (unsupported write conditions throw during `buildOperations`, and
1078
- * the whole bundle is asserted JSON-serializable here).
1079
- *
1080
- * @param queries Read definitions (`defineQueries(...)` output). Optional.
1081
- * @param commands Write definitions (`defineCommands(...)` output). Optional.
1082
- * @param registry Metadata source (defaults to the global registry).
1083
- * @param transactions Declarative transactions (`defineTransactions(...)`). Optional.
1084
- * @param contractInputs The CQRS contract layer (#59): resolved contracts +
1085
- * context-ownership. Optional — absent reproduces the pre-#59 document.
1086
- */
1087
- declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
1088
-
1089
- export { compilePreparedPlan as $, type AnyModelContract as A, type BuiltContracts as B, type ContextOwnership as C, buildContracts as D, buildManifest as E, buildOperations as F, buildQuerySpec as G, buildTransactionSpec as H, buildTransactions as I, canonicalizeExpressionSpec as J, collectContractBoundaryViolations as K, collectContractN1Violations as L, defineScpTransaction as M, defineTransaction as N, defineTransactions as O, type PreparedWriteOpSpec as P, isTransactionRef as Q, operationsContainScpNodes as R, SCP_LOWERED_MARKER as S, type TransactionDefinition as T, operationsSpecVersion as U, when as V, type WhenComparison as W, PREPARED_FORMAT_VERSION as X, type PreparedBindMap as Y, buildPreparedPlanDocument as Z, canonicalJson as _, type PreparedPlanDocument as a, entityFingerprint as a0, planFingerprint as a1, type ContextOwnershipMap as b, type ContractBoundaryViolation as c, type ContractInputs as d, type ContractMap as e, type ContractN1Violation as f, type PreparedBindSpec as g, type PreparedPlanSpec as h, type PreparedReadRouteSpec as i, type ScpParamValues as j, type TransactionParamShape as k, type TransactionRef as l, type TxConditionCheckOptions as m, type TxForEachInstruction as n, type TxForEachOptions as o, type TxInstruction as p, type TxRecorder as q, type TxWriteInstruction as r, type TxWriteOptions as s, assertBundleSerializable as t, assertContractBoundaries as u, assertContractN1Safe as v, assertJsonSerializable as w, assertSupportedCondition as x, buildBridgeBundle as y, buildContexts as z };