graphddb 0.9.1 → 0.9.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.
package/README.md CHANGED
@@ -1,41 +1,63 @@
1
1
  # GraphDDB
2
2
 
3
- **Build GraphQL-like queries directly on DynamoDB —— while keeping every access pattern explicit.**
3
+ **Portable, contract-first DynamoDB access patterns.**
4
4
 
5
- GraphDDB is a type-safe Runtime that maps DynamoDB's native access patterns onto a GraphQL-like query
6
- model. PK / GSI / projection / relation / cursor / operation limits are all kept visible rather than hidden.
7
- It is not an ORM —— it is for **defining, validating, and executing** DynamoDB access patterns through
8
- TypeScript types.
5
+ GraphDDB lets you define your DynamoDB access patterns once in TypeScript, validate them at build time,
6
+ inspect the exact execution plan before anything runs, and execute the *same* contract from five
7
+ languages — TypeScript, Python, Rust, Go, and PHP. It is **not an ORM**: it is a contract compiler +
8
+ runtime for explicit DynamoDB access patterns.
9
9
 
10
- ## Features
10
+ The access patterns become a portable, executable artifact. TypeScript is the single source of truth;
11
+ a static planner lowers each contract to a language-neutral IR (`manifest.json` + `operations.json`),
12
+ and every language runtime executes that IR with results pinned **byte-identical** by a shared
13
+ conformance suite. GraphQL-like traversal is one of the authoring surfaces — ergonomic, but not the
14
+ point. The point is that PK / GSI / projection / relation / cursor / operation limits stay explicit,
15
+ verifiable, and portable rather than hidden behind an abstraction.
16
+
17
+ ## ✨ What GraphDDB does — in three layers
18
+
19
+ **1. Define** explicit DynamoDB access patterns
11
20
 
12
21
  - ✅ **Type-safe models** —— the TS class is the single source of truth. Keys / GSIs / field types are checked at compile time.
13
- - ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select`.
14
- - ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give.
15
- - ✅ **Projection** —— only `select`ed fields are read, and only those appear in the result type.
16
- - ✅ **Execution plan (Explain)** —— inspect the execution plan before touching DynamoDB.
17
- - **CQRS contracts** —— public Query/Command contracts (N+1-safe, context boundaries).
18
- - ✅ **Python generation** —— generate a Python client + runtime from TS as the SSoT (conformance-verified).
19
- - ✅ **Testing adapter** —— unit-test planner / traversal / transaction / CDC with an in-memory executor.
20
- - ✅ **Middleware** —— host-side hooks for reads / writes (logging, tenant, authorization).
21
- - ✅ **CDC emulator** —— drive and test change events equivalent to DynamoDB Streams locally.
22
- - ✅ **CDC projection** —— `@cdcProjected()` + `fromChange` / `subscribe` parse CDC change events into typed records (sink delivery / idempotency stay with the consumer).
23
- - ✅ **Maintained access paths** —— keep embedded snapshots / aggregate counters synchronized atomically.
24
- - ✅ **CloudFormation generation** —— emit a CloudFormation template for the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / TableClass / SSE / PROVISIONED / Auto Scaling).
25
- - ✅ **Docs generation** —— generate a model specification (Markdown + Mermaid) from the TS models; templates are Handlebars and user-overridable.
22
+ - ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select` (an ergonomic authoring surface — arguments → key lookup, selection → projection).
23
+ - ✅ **CQRS contracts** —— public Query/Command contracts as the read/write authoring surface (N+1-safe, context boundaries, composition across contracts).
24
+ - ✅ **Maintained access paths** —— declare embedded snapshots / aggregate counters kept synchronized atomically with the source write.
25
+
26
+ **2. Validate & explain** before anything touches DynamoDB
27
+
28
+ - ✅ **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and over-depth traversals are rejected at compile time / by the linter — equally for human- and AI-written code.
29
+ - ✅ **Execution plan (Explain)** —— `explain()` returns the exact DynamoDB operations, so RCU shape is inspectable up front.
30
+ - ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give; `select`ed fields are the only ones read and the only ones in the result type.
31
+ - ✅ **In-memory testing** —— unit-test planner / traversal / transaction / CDC with no Docker, no DynamoDB Local.
32
+
33
+ **3. Execute the same contract, portably, from five languages**
34
+
35
+ - ✅ **Portable executable IR** —— the static planner lowers each contract to a language-neutral bridge bundle (`manifest.json` + `operations.json`, a `components[]` graph on the shared behavior-contracts vocabulary).
36
+ - ✅ **5 language runtimes** —— generate clients + runtimes for **Python / Rust / Go / PHP** (TypeScript runs the IR natively), all executing the same access patterns.
37
+ - ✅ **5-language conformance** —— TS / Python / Rust / Go / PHP run the same cases against DynamoDB Local; CI pins the results **byte-identical**.
38
+
39
+ **Operational surface** (all built on the layers above): **Middleware** (host-side read/write hooks) ·
40
+ **CDC emulator** (local DynamoDB-Streams-equivalent change events) · **CDC projection**
41
+ (`@cdcProjected()` + `fromChange` / `subscribe` into typed records) · **Prepared statements**
42
+ (compile-once / execute-many, AOT static plans) · **CloudFormation generation** ·
43
+ **Docs generation** (Markdown + Mermaid, Handlebars-overridable).
26
44
 
27
45
  ## 🤔 Philosophy
28
46
 
47
+ - **Access patterns as portable contracts** —— an access pattern is defined once and becomes a
48
+ language-neutral, executable artifact. The same contract is validated, explained, and then executed
49
+ identically across languages, instead of being re-implemented per stack.
29
50
  - **Native DynamoDB access patterns** —— express access patterns directly in code instead of hiding them
30
51
  behind an ORM abstraction. Because PK / GSI, projection, and limits stay visible, the correct design
31
52
  becomes the easiest path.
32
- - **Graph-style traversal** —— GraphQL's query model (arguments → key lookup, selection → projection,
33
- relation → Query/Get/BatchGet) corresponds naturally to DynamoDB's access patterns.
34
53
  - **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and traversals exceeding
35
54
  the allowed depth are rejected before execution at compile time / by the linter. The same constraints
36
55
  apply equally to human-written and AI-generated code.
37
56
  - **Runtime transparency** —— the runtime is not a black box. `explain()` shows the execution plan, and the
38
57
  plan reveals the shape of RCU usage.
58
+ - **Graph-style traversal is a surface, not the essence** —— GraphQL's query model (arguments → key
59
+ lookup, selection → projection, relation → Query/Get/BatchGet) is a convenient way to *author* access
60
+ patterns; the essence is the explicit, portable contract underneath.
39
61
 
40
62
  ## 📦 Install
41
63
 
@@ -57,7 +79,7 @@ Node.js ≥ 22 is required.
57
79
  ## 🚀 Quick Start
58
80
 
59
81
  ```ts
60
- import { DDBModel, model, string, key, k } from 'graphddb';
82
+ import { DDBModel, graphddb, model, string, key, k } from 'graphddb';
61
83
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
62
84
 
63
85
  @model({ table: 'AppTable', prefix: 'USER' })
@@ -200,8 +222,16 @@ Each capability is summarized here only. For details, operator lists, and proced
200
222
  authorization scoping). Host-only and non-serialized. For hook points, see [`docs/middleware.md`](./docs/middleware.md).
201
223
  - **Class hydration** —— `options.hydrate` loads read results into host-language domain objects (opt-in,
202
224
  Phase 1: `query` top level). [`docs/class-hydration.md`](./docs/class-hydration.md).
203
- - **Python bridge** —— generate a Python client + runtime with TS as the SSoT, kept in step by a TS↔Python
204
- conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
225
+ - **Multi-language bridge** —— generate Python / Rust / Go / PHP clients + runtimes with TS as the SSoT,
226
+ kept in step by a 5-language conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
227
+ - **Prepared statements** —— `graphddb.prepare($ => ({...}))` → `.execute(params)` (unified read/write).
228
+ "Contract-free precompilation": compile a declarative route once and execute it repeatedly with
229
+ different params. With the **true build-time compilation (AOT)** of `graphddb transform prepared
230
+ --aot`, plans are emitted as static artifacts and zero runtime compilation happens, including the
231
+ first call (a stale plan is loud-rejected at load; drift is caught in CI). Without the transform,
232
+ behavior stays identical through the lazy-slot + structural-memoization fallback. no-runtime-capture
233
+ is loudly enforced by a build-time lint + a runtime guard.
234
+ [`docs/prepared-statements.md`](./docs/prepared-statements.md).
205
235
  - **CQRS contracts** —— public Query/Command contracts (cardinality matrix, N+1 safety, context boundaries,
206
236
  composition across contracts). [`docs/cqrs-contract.md`](./docs/cqrs-contract.md) /
207
237
  [`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
@@ -212,6 +242,17 @@ Each capability is summarized here only. For details, operator lists, and proced
212
242
  `graphddb.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
213
243
  consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
214
244
  delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
245
+ - **Multi-language codegen (the 3 paths, #257)** —— execution keeps 2 paths + 1 consumer surface,
246
+ coexisting permanently: (1) **dynamic JSON path (default)** — the `components[]` of
247
+ `operations.json` are loaded at runtime and interpreted by the shared `run_behavior` (for swapping
248
+ behavior without a deploy). (2) **codegen-static path (opt-in)** — `graphddb generate behaviors
249
+ --lang <ts|python|go|rust|php>` hands the same IR to the **shared upstream behavior-contracts
250
+ generator** (bc#13), emitting modules with the IR baked in as native literals (graphddb owns only
251
+ envelope + invoke — no generation logic). A generated module checks its fingerprint against the
252
+ live registry and loud-rejects when stale (#208 discipline). Rust generated modules use
253
+ `std::sync::LazyLock`, so **Rust >= 1.80** is required. (3) **typed bindings
254
+ (`generate python/php/rust/go`)** — the consumer-UX surface (typed repositories / DTOs), still
255
+ graphddb-owned; not an execution engine — it delegates to the paths above.
215
256
  - **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
216
257
  the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto
217
258
  Scaling). [`docs/cloudformation.md`](./docs/cloudformation.md).
@@ -232,10 +273,11 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
232
273
  | [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
233
274
  | [Design patterns](./docs/design-patterns.md) | Mapping representative DynamoDB design patterns onto graphddb features (how to declare `pattern` / `@model({ kind })` + `@maintainedFrom` / `@aggregate`, read & write maintenance, per-phase coverage). |
234
275
  | [Middleware / Hooks](./docs/middleware.md) | The `graphddb.config.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
276
+ | [Prepared statements](./docs/prepared-statements.md) | `graphddb.prepare` / `.execute` (unified read/write prepared statements), the no-runtime-capture constraint, `graphddb transform prepared` (AOT static plans / compile-time hoisting + build-time lint + drift detection): choosing between the 3 tiers AOT > lazy slot + structural memoization > ad-hoc. |
235
277
  | [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
236
278
  | [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF (`entityWrites`, fragment composition, derivation of atomic `TransactWriteItems`). |
237
279
  | [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
238
- | [Multi-language / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
280
+ | [Multi-language bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and Python / Rust / Go / PHP runtimes; 5-language conformance. |
239
281
  | [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
240
282
  | [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
241
283
  | [CDC projection](./docs/cdc-projection.md) | The `@cdcProjected()` + `fromChange` / `subscribe` typed-consumer-IF: a contract that parses CDC change events into typed records. Boundary (parse→typed is graphddb; subscription, sink delivery, idempotency are the consumer's). |
@@ -256,23 +298,30 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
256
298
 
257
299
  A query travels from the TS Model through the Planner / Runtime to DynamoDB:
258
300
 
259
- ```mermaid
260
- flowchart LR
261
- M["TS Model<br/>Entity / Key / GSI / Relation"] --> P["Planner<br/>access pattern resolution / projection"]
262
- P --> R["Runtime<br/>execute / retry / hydrate"]
263
- R --> D[("DynamoDB")]
264
- ```
301
+ ![Query execution pipeline: TS Model → Planner → Runtime → DynamoDB](./docs/diagrams/query-pipeline.en.svg)
265
302
 
266
- From the same TS Model (the SSoT), multiple targets are derived:
303
+ From the same TS Model (the SSoT), multiple targets are derived. The language runtimes are fanned out to
304
+ 4 languages through the portable IR (the `components[]` graph in `manifest.json` + `operations.json`),
305
+ and the execution results of all 5 languages — TS included — are pinned byte-identical by the
306
+ conformance suite:
267
307
 
268
- ```mermaid
269
- flowchart LR
270
- M["TS Model (SSoT)"] --> RT["Runtime (TS)"]
271
- M --> PY["Python client + runtime"]
272
- M --> CQ["CQRS Contract"]
273
- M --> CFN["CloudFormation template"]
274
- M --> DOC["Model docs (Markdown + Mermaid)"]
275
- ```
308
+ ![SSoT derivation: TS Model → TS runtime / portable IR → 4 language runtimes / CQRS / CloudFormation / docs](./docs/diagrams/ssot-derivation.en.svg)
309
+
310
+ | Runtime | Package | Distribution |
311
+ |---------|---------|--------------|
312
+ | TypeScript | `graphddb` | npm |
313
+ | Python | `graphddb-runtime` | PyPI |
314
+ | Rust | `graphddb_runtime` | crates.io |
315
+ | Go | `github.com/foo-log-inc/graphddb/go` | go module (`go/vX.Y.Z` tags) |
316
+ | PHP | `graphddb/runtime` | bundled in-repo (not on composer) |
317
+
318
+ Multi-language execution keeps 3 permanent paths — 2 execution paths + 1 consumer surface (see
319
+ "Multi-language codegen" under [More Capabilities](#-more-capabilities)):
320
+
321
+ ![The three multi-language execution paths: authoring → static planner → portable IR → dynamic JSON / codegen-static / typed bindings](./docs/diagrams/execution-paths.en.svg)
322
+
323
+ > The figures are self-contained SVGs drawn with diagram-contracts (Draw TSX).
324
+ > Sources live in [`diagrams/`](./diagrams/); regenerate with `diagrams/build.sh`.
276
325
 
277
326
  ## 📄 License
278
327
 
@@ -1,7 +1,7 @@
1
- import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-CWytoEaE.js';
2
- export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-CWytoEaE.js';
3
- import { E as EntityMetadata } from '../types-nk5okD7d.js';
4
- import '../types-CgXS-4Ox.js';
1
+ import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-C_6ScBbu.js';
2
+ export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-C_6ScBbu.js';
3
+ import { E as EntityMetadata } from '../types-DLpvjjV_.js';
4
+ import '../types-DGwo00JB.js';
5
5
  import 'behavior-contracts';
6
6
 
7
7
  /**
@@ -1876,6 +1876,209 @@ function portableIrDocument(components) {
1876
1876
  };
1877
1877
  }
1878
1878
 
1879
+ // src/spec/type-notation.ts
1880
+ import {
1881
+ assertPortableComponentGraph
1882
+ } from "behavior-contracts";
1883
+ var UndeterminablePortableType = class extends Error {
1884
+ constructor(message) {
1885
+ super(message);
1886
+ this.name = "UndeterminablePortableType";
1887
+ }
1888
+ };
1889
+ function fieldTypeToPortable(fieldType, where) {
1890
+ switch (fieldType) {
1891
+ case "string":
1892
+ case "binary":
1893
+ return "string";
1894
+ case "number":
1895
+ return "float";
1896
+ case "boolean":
1897
+ return "bool";
1898
+ case "stringSet":
1899
+ return { arr: "string" };
1900
+ case "numberSet":
1901
+ return { arr: "float" };
1902
+ case "list":
1903
+ case "map":
1904
+ throw new UndeterminablePortableType(
1905
+ `type-notation: the '${fieldType}' field at ${where} has no portable type (a DynamoDB ${fieldType} attribute has no modeled element/record shape, and the portable type notation has no free-form/any type). The behavior is left untyped (byte-identical).`
1906
+ );
1907
+ }
1908
+ }
1909
+ var scalarType = fieldTypeToPortable;
1910
+ function relationSegments(nodeId) {
1911
+ return nodeId.split(".").filter((p) => p !== "items");
1912
+ }
1913
+ function nodePorts(node) {
1914
+ if ("map" in node) return node.map.ports;
1915
+ if ("cond" in node) return void 0;
1916
+ return node.ports;
1917
+ }
1918
+ function nodeComponent(node) {
1919
+ if ("map" in node) return node.map.component;
1920
+ if ("cond" in node) return void 0;
1921
+ return node.component;
1922
+ }
1923
+ function isConnectionRootOp(rootComponent, cardinalityOne) {
1924
+ return (rootComponent === "Query" || rootComponent === "BatchGetItem") && !cardinalityOne;
1925
+ }
1926
+ function nodeProjection(ports) {
1927
+ const port = ports.projection;
1928
+ return (port?.arr ?? []).filter((f) => typeof f === "string");
1929
+ }
1930
+ function insertNode(root, node, manifest, nodeById) {
1931
+ const ports = nodePorts(node);
1932
+ if (ports === void 0) return;
1933
+ const segments = relationSegments(node.id);
1934
+ if (segments.length === 0) return;
1935
+ let tree = root;
1936
+ let parent = root;
1937
+ let entity = root.entity;
1938
+ let kind = "single";
1939
+ for (const prop of segments) {
1940
+ parent = tree;
1941
+ const relation = manifest.entities[entity]?.relations[prop];
1942
+ if (relation === void 0) {
1943
+ throw new Error(
1944
+ `type-notation: relation segment '${prop}' of node has no relation on entity '${entity}'; cannot derive its result type. The components graph and the manifest have diverged.`
1945
+ );
1946
+ }
1947
+ kind = relation.type === "hasMany" || relation.type === "refs" ? "connection" : "single";
1948
+ const target = relation.target;
1949
+ let child = tree.children.get(prop);
1950
+ if (!child) {
1951
+ child = {
1952
+ entity: target,
1953
+ projection: [],
1954
+ children: /* @__PURE__ */ new Map(),
1955
+ implicitFields: /* @__PURE__ */ new Set(),
1956
+ relationKind: kind
1957
+ };
1958
+ tree.children.set(prop, child);
1959
+ }
1960
+ tree = child;
1961
+ entity = target;
1962
+ }
1963
+ tree.projection = nodeProjection(ports);
1964
+ tree.relationKind = kind;
1965
+ nodeById.set(node.id, tree);
1966
+ const implicit = ports.implicitSourceField;
1967
+ if (typeof implicit === "string") parent.implicitFields.add(implicit);
1968
+ }
1969
+ function wrapRelation(childObj, kind) {
1970
+ return kind === "connection" ? { obj: { cursor: { opt: "string" }, items: { arr: childObj } } } : { opt: childObj };
1971
+ }
1972
+ function nodeObjType(node, manifest, where, includeRelations) {
1973
+ const meta = manifest.entities[node.entity];
1974
+ const relationProps = new Set(node.children.keys());
1975
+ const scalarFields = node.projection.filter(
1976
+ (f) => !relationProps.has(f) && !node.implicitFields.has(f)
1977
+ );
1978
+ const fields = {};
1979
+ for (const f of scalarFields) {
1980
+ const ft = meta?.fields[f]?.type ?? "string";
1981
+ fields[f] = scalarType(ft, `${where}.${f}`);
1982
+ }
1983
+ if (includeRelations) {
1984
+ for (const [prop, child] of node.children) {
1985
+ const childObj = nodeObjType(child, manifest, `${where}.${prop}`, true);
1986
+ fields[prop] = wrapRelation(childObj, child.relationKind);
1987
+ }
1988
+ }
1989
+ if (Object.keys(fields).length === 0) {
1990
+ throw new UndeterminablePortableType(
1991
+ `type-notation: result node at ${where} (entity '${node.entity}') projects no fields and has no relations \u2014 an empty record has no useful portable type; the behavior is left untyped.`
1992
+ );
1993
+ }
1994
+ return { obj: fields };
1995
+ }
1996
+ function canonicalizeDeep(value) {
1997
+ if (Array.isArray(value)) return value.map(canonicalizeDeep);
1998
+ if (value !== null && typeof value === "object") {
1999
+ const src = value;
2000
+ const out = {};
2001
+ for (const k of Object.keys(src).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
2002
+ out[k] = canonicalizeDeep(src[k]);
2003
+ }
2004
+ return out;
2005
+ }
2006
+ return value;
2007
+ }
2008
+ function annotateQueryComponent(component, rootEntity, cardinalityOne, manifest) {
2009
+ const root = {
2010
+ entity: rootEntity,
2011
+ projection: [],
2012
+ children: /* @__PURE__ */ new Map(),
2013
+ implicitFields: /* @__PURE__ */ new Set()
2014
+ };
2015
+ const rootNode = component.body.find((n) => n.id === "root");
2016
+ if (rootNode !== void 0) {
2017
+ const ports = nodePorts(rootNode);
2018
+ if (ports !== void 0) root.projection = nodeProjection(ports);
2019
+ }
2020
+ const nodeById = /* @__PURE__ */ new Map();
2021
+ for (const node of component.body) {
2022
+ if (node.id === "root") continue;
2023
+ insertNode(root, node, manifest, nodeById);
2024
+ }
2025
+ const annotatedBody = component.body.map((node) => {
2026
+ if ("cond" in node) {
2027
+ throw new Error(
2028
+ `type-notation: unexpected 'cond' node '${node.id}' in query component '${component.name}'; query lowering does not emit conditionals, so its outType cannot be derived.`
2029
+ );
2030
+ }
2031
+ if (node.id === "root") {
2032
+ const outType2 = isConnectionRootOp(nodeComponent(node), cardinalityOne) ? wrapRelation(nodeObjType(root, manifest, `${component.name}.root`, true), "connection") : nodeObjType(root, manifest, `${component.name}.root`, false);
2033
+ return { ...node, outType: outType2 };
2034
+ }
2035
+ const rn = nodeById.get(node.id);
2036
+ if (rn === void 0) {
2037
+ throw new Error(
2038
+ `type-notation: no result-tree node for body node '${node.id}' in component '${component.name}'; the model\u2192annotation walk is incomplete (fail-closed \u2014 not skipped).`
2039
+ );
2040
+ }
2041
+ const obj = nodeObjType(rn, manifest, `${component.name}.${node.id}`, true);
2042
+ const outType = wrapRelation(obj, rn.relationKind);
2043
+ return { ...node, outType };
2044
+ });
2045
+ const hasRelations = root.children.size > 0;
2046
+ const rootIsConnection = rootNode !== void 0 && isConnectionRootOp(nodeComponent(rootNode), cardinalityOne);
2047
+ let outputType;
2048
+ if (rootIsConnection) {
2049
+ outputType = wrapRelation(nodeObjType(root, manifest, `${component.name}#output`, true), "connection");
2050
+ } else if (!hasRelations) {
2051
+ outputType = nodeObjType(root, manifest, `${component.name}#output.root`, false);
2052
+ } else {
2053
+ const rootObj = nodeObjType(root, manifest, `${component.name}#output.root`, false);
2054
+ const fields = { root: rootObj };
2055
+ for (const [prop, child] of root.children) {
2056
+ const childObj = nodeObjType(child, manifest, `${component.name}#output.${prop}`, true);
2057
+ fields[prop] = wrapRelation(childObj, child.relationKind);
2058
+ }
2059
+ outputType = { obj: fields };
2060
+ }
2061
+ return canonicalizeDeep({ ...component, body: annotatedBody, outputType });
2062
+ }
2063
+ function annotateComponents(components, typeFacts, manifest) {
2064
+ const out = components.map((c) => {
2065
+ const facts = typeFacts.get(c.name);
2066
+ if (facts === void 0) return c;
2067
+ try {
2068
+ return annotateQueryComponent(c, facts.rootEntity, facts.cardinalityOne, manifest);
2069
+ } catch (err) {
2070
+ if (err instanceof UndeterminablePortableType) return c;
2071
+ throw err;
2072
+ }
2073
+ });
2074
+ assertPortableComponentGraph({
2075
+ irVersion: 1,
2076
+ exprVersion: EXPR_VERSION,
2077
+ components: out
2078
+ });
2079
+ return out;
2080
+ }
2081
+
1879
2082
  // src/spec/operations.ts
1880
2083
  function paramSpecs(params) {
1881
2084
  const out = {};
@@ -2311,6 +2514,9 @@ function buildQuerySpec(def) {
2311
2514
  operations,
2312
2515
  // operation 'query' → a single entity object; 'list' → a connection.
2313
2516
  cardinality: def.operation === "query" ? "one" : "many",
2517
+ // The root model — the SSoT anchor for the bc#45 type-notation derivation
2518
+ // (build-internal; dropped by toPortableQuery, so operations.json is unchanged).
2519
+ entity: def.entity.name,
2314
2520
  ...executionPlan !== void 0 ? { executionPlan } : {},
2315
2521
  // Pure-documentation description (issue #154); absent → byte-identical spec.
2316
2522
  ...def.description !== void 0 ? { description: def.description } : {}
@@ -2398,6 +2604,15 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
2398
2604
  },
2399
2605
  contractSpecs
2400
2606
  );
2607
+ const typeFacts = /* @__PURE__ */ new Map();
2608
+ for (const [name, spec] of Object.entries(querySpecs)) {
2609
+ if (spec.entity === void 0) continue;
2610
+ typeFacts.set(name, {
2611
+ rootEntity: spec.entity,
2612
+ cardinalityOne: spec.cardinality === "one"
2613
+ });
2614
+ }
2615
+ const annotatedComponents = typeFacts.size > 0 ? annotateComponents(components, typeFacts, buildManifest()) : components;
2401
2616
  const portableQueries = {};
2402
2617
  for (const [name, spec] of Object.entries(querySpecs)) {
2403
2618
  portableQueries[name] = toPortableQuery(name, spec);
@@ -2416,7 +2631,7 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
2416
2631
  return {
2417
2632
  version: operationsSpecVersion(content),
2418
2633
  ...content,
2419
- ...components.length > 0 ? { components } : {}
2634
+ ...annotatedComponents.length > 0 ? { components: annotatedComponents } : {}
2420
2635
  };
2421
2636
  }
2422
2637