graphddb 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -242,15 +242,25 @@ Each capability is summarized here only. For details, operator lists, and proced
242
242
  `graphddb.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
243
243
  consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
244
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
245
+ - **Multi-language codegen** —— execution keeps 3 paths + 1 consumer surface, coexisting
246
+ permanently: (1) **dynamic JSON path (default)** — the `components[]` of
247
247
  `operations.json` are loaded at runtime and interpreted by the shared `run_behavior` (for swapping
248
248
  behavior without a deploy). (2) **codegen-static path (opt-in)** — `graphddb generate behaviors
249
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
250
+ generator**, emitting modules with the IR baked in as native literals. Execution is still
251
+ interpreted by the shared `run_behavior` **the per-op path is identical to the dynamic path**
252
+ (only the IR source differs; no native execution logic is generated). A generated module checks
253
+ its fingerprint against the live registry and loud-rejects when stale. Rust generated modules use
254
+ `std::sync::LazyLock`, so **Rust >= 1.80** is required. (3) **typed-native path — genuine
255
+ native-execution codegen (Go / Rust)** — `--typed-native` skips the interpreter entirely and
256
+ emits **native per-node execution code** (`run_native_raw_struct_<comp>`: direct field access on
257
+ concrete structs, a local error type, **no `run_behavior`, zero bc-runtime import**). Because the
258
+ generated code has the same shape a careful engineer hand-writes against the SDK (build key →
259
+ call → unmarshal into a struct → fan out), it runs at **SDK-floor parity** (≈0.9–1.05× the
260
+ hand-written SDK baseline in both Go and Rust) and **2–3× faster than the interpreter**.
261
+ Runtime-freeness is compiler-verified (`scripts/native-codegen-purity.mjs`); shapes not natively
262
+ coverable fail closed (never silently boxed); Rust can emit async (`ioModel=async`). Measured
263
+ results: [`benchmark/CROSS-LANG.md`](./benchmark/CROSS-LANG.md). (4) **typed bindings
254
264
  (`generate python/php/rust/go`)** — the consumer-UX surface (typed repositories / DTOs), still
255
265
  graphddb-owned; not an execution engine — it delegates to the paths above.
256
266
  - **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
@@ -315,10 +325,11 @@ conformance suite:
315
325
  | Go | `github.com/foo-log-inc/graphddb/go` | go module (`go/vX.Y.Z` tags) |
316
326
  | PHP | `graphddb/runtime` | bundled in-repo (not on composer) |
317
327
 
318
- Multi-language execution keeps 3 permanent paths — 2 execution paths + 1 consumer surface (see
319
- "Multi-language codegen" under [More Capabilities](#-more-capabilities)):
328
+ Multi-language execution keeps 4 permanent paths — 3 execution paths (dynamic JSON /
329
+ codegen-static / typed-native) + 1 consumer surface (see "Multi-language codegen" under
330
+ [More Capabilities](#-more-capabilities)):
320
331
 
321
- ![The three multi-language execution paths: authoring → static planner → portable IR → dynamic JSON / codegen-static / typed bindings](./docs/diagrams/execution-paths.en.svg)
332
+ ![The multi-language execution paths: authoring → static planner → portable IR → dynamic JSON / codegen-static / typed bindings](./docs/diagrams/execution-paths.en.svg)
322
333
 
323
334
  > The figures are self-contained SVGs drawn with diagram-contracts (Draw TSX).
324
335
  > Sources live in [`diagrams/`](./diagrams/); regenerate with `diagrams/build.sh`.
@@ -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,237 @@ 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 mapIntoKey(node) {
1924
+ return "map" in node ? node.map.into : void 0;
1925
+ }
1926
+ function resolveResultNode(root, nodeId) {
1927
+ let tree = root;
1928
+ for (const prop of relationSegments(nodeId)) {
1929
+ tree = tree?.children.get(prop);
1930
+ if (tree === void 0) return void 0;
1931
+ }
1932
+ return tree;
1933
+ }
1934
+ function isConnectionRootOp(rootComponent, cardinalityOne) {
1935
+ return (rootComponent === "Query" || rootComponent === "BatchGetItem") && !cardinalityOne;
1936
+ }
1937
+ function nodeProjection(ports) {
1938
+ const port = ports.projection;
1939
+ return (port?.arr ?? []).filter((f) => typeof f === "string");
1940
+ }
1941
+ function insertNode(root, node, manifest, nodeById) {
1942
+ const ports = nodePorts(node);
1943
+ if (ports === void 0) return;
1944
+ const segments = relationSegments(node.id);
1945
+ if (segments.length === 0) return;
1946
+ let tree = root;
1947
+ let parent = root;
1948
+ let entity = root.entity;
1949
+ let kind = "single";
1950
+ for (const prop of segments) {
1951
+ parent = tree;
1952
+ const relation = manifest.entities[entity]?.relations[prop];
1953
+ if (relation === void 0) {
1954
+ throw new Error(
1955
+ `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.`
1956
+ );
1957
+ }
1958
+ kind = relation.type === "hasMany" || relation.type === "refs" ? "connection" : "single";
1959
+ const target = relation.target;
1960
+ let child = tree.children.get(prop);
1961
+ if (!child) {
1962
+ child = {
1963
+ entity: target,
1964
+ projection: [],
1965
+ children: /* @__PURE__ */ new Map(),
1966
+ implicitFields: /* @__PURE__ */ new Set(),
1967
+ relationKind: kind
1968
+ };
1969
+ tree.children.set(prop, child);
1970
+ }
1971
+ tree = child;
1972
+ entity = target;
1973
+ }
1974
+ tree.projection = nodeProjection(ports);
1975
+ tree.relationKind = kind;
1976
+ nodeById.set(node.id, tree);
1977
+ const implicit = ports.implicitSourceField;
1978
+ if (typeof implicit === "string") parent.implicitFields.add(implicit);
1979
+ }
1980
+ function wrapRelation(childObj, kind) {
1981
+ return kind === "connection" ? { obj: { cursor: { opt: "string" }, items: { arr: childObj } } } : { opt: childObj };
1982
+ }
1983
+ function nodeObjType(node, manifest, where, includeRelations) {
1984
+ const meta = manifest.entities[node.entity];
1985
+ const relationProps = new Set(node.children.keys());
1986
+ const scalarFields = node.projection.filter(
1987
+ (f) => !relationProps.has(f) && !node.implicitFields.has(f)
1988
+ );
1989
+ const fields = {};
1990
+ for (const f of scalarFields) {
1991
+ const ft = meta?.fields[f]?.type ?? "string";
1992
+ fields[f] = scalarType(ft, `${where}.${f}`);
1993
+ }
1994
+ if (includeRelations) {
1995
+ for (const [prop, child] of node.children) {
1996
+ const childObj = nodeObjType(child, manifest, `${where}.${prop}`, true);
1997
+ fields[prop] = wrapRelation(childObj, child.relationKind);
1998
+ }
1999
+ }
2000
+ if (Object.keys(fields).length === 0) {
2001
+ throw new UndeterminablePortableType(
2002
+ `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.`
2003
+ );
2004
+ }
2005
+ return { obj: fields };
2006
+ }
2007
+ function canonicalizeDeep(value) {
2008
+ if (Array.isArray(value)) return value.map(canonicalizeDeep);
2009
+ if (value !== null && typeof value === "object") {
2010
+ const src = value;
2011
+ const out = {};
2012
+ for (const k of Object.keys(src).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
2013
+ out[k] = canonicalizeDeep(src[k]);
2014
+ }
2015
+ return out;
2016
+ }
2017
+ return value;
2018
+ }
2019
+ function annotateQueryComponent(component, rootEntity, cardinalityOne, manifest) {
2020
+ const root = {
2021
+ entity: rootEntity,
2022
+ projection: [],
2023
+ children: /* @__PURE__ */ new Map(),
2024
+ implicitFields: /* @__PURE__ */ new Set()
2025
+ };
2026
+ const rootNode = component.body.find((n) => n.id === "root");
2027
+ if (rootNode !== void 0) {
2028
+ const ports = nodePorts(rootNode);
2029
+ if (ports !== void 0) root.projection = nodeProjection(ports);
2030
+ }
2031
+ const nodeById = /* @__PURE__ */ new Map();
2032
+ for (const node of component.body) {
2033
+ if (node.id === "root") continue;
2034
+ insertNode(root, node, manifest, nodeById);
2035
+ }
2036
+ const annotatedBody = component.body.map((node) => {
2037
+ if ("cond" in node) {
2038
+ throw new Error(
2039
+ `type-notation: unexpected 'cond' node '${node.id}' in query component '${component.name}'; query lowering does not emit conditionals, so its outType cannot be derived.`
2040
+ );
2041
+ }
2042
+ if (node.id === "root") {
2043
+ const outType2 = isConnectionRootOp(nodeComponent(node), cardinalityOne) ? wrapRelation(nodeObjType(root, manifest, `${component.name}.root`, true), "connection") : nodeObjType(root, manifest, `${component.name}.root`, false);
2044
+ return { ...node, outType: outType2 };
2045
+ }
2046
+ const into = mapIntoKey(node);
2047
+ if (into !== void 0) {
2048
+ const mapParent = node.map.parent;
2049
+ const parentNode = mapParent !== void 0 ? resolveResultNode(root, mapParent) : void 0;
2050
+ if (parentNode === void 0) {
2051
+ throw new Error(
2052
+ `type-notation: map\u2026into node '${node.id}' in component '${component.name}' names parent '${String(mapParent)}', which has no result-tree node; cannot derive its augmented element type (fail-closed \u2014 the components graph and the manifest have diverged).`
2053
+ );
2054
+ }
2055
+ const elemObj = nodeObjType(
2056
+ parentNode,
2057
+ manifest,
2058
+ `${component.name}.${node.id}[into ${into}]`,
2059
+ true
2060
+ );
2061
+ return { ...node, outType: { arr: elemObj } };
2062
+ }
2063
+ const rn = nodeById.get(node.id);
2064
+ if (rn === void 0) {
2065
+ throw new Error(
2066
+ `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).`
2067
+ );
2068
+ }
2069
+ const obj = nodeObjType(rn, manifest, `${component.name}.${node.id}`, true);
2070
+ const outType = wrapRelation(obj, rn.relationKind);
2071
+ return { ...node, outType };
2072
+ });
2073
+ const hasRelations = root.children.size > 0;
2074
+ const rootIsConnection = rootNode !== void 0 && isConnectionRootOp(nodeComponent(rootNode), cardinalityOne);
2075
+ let outputType;
2076
+ if (rootIsConnection) {
2077
+ outputType = wrapRelation(nodeObjType(root, manifest, `${component.name}#output`, true), "connection");
2078
+ } else if (!hasRelations) {
2079
+ outputType = nodeObjType(root, manifest, `${component.name}#output.root`, false);
2080
+ } else {
2081
+ const rootObj = nodeObjType(root, manifest, `${component.name}#output.root`, false);
2082
+ const fields = { root: rootObj };
2083
+ for (const [prop, child] of root.children) {
2084
+ const childObj = nodeObjType(child, manifest, `${component.name}#output.${prop}`, true);
2085
+ fields[prop] = wrapRelation(childObj, child.relationKind);
2086
+ }
2087
+ outputType = { obj: fields };
2088
+ }
2089
+ return canonicalizeDeep({ ...component, body: annotatedBody, outputType });
2090
+ }
2091
+ function annotateComponents(components, typeFacts, manifest) {
2092
+ const out = components.map((c) => {
2093
+ const facts = typeFacts.get(c.name);
2094
+ if (facts === void 0) return c;
2095
+ try {
2096
+ return annotateQueryComponent(c, facts.rootEntity, facts.cardinalityOne, manifest);
2097
+ } catch (err) {
2098
+ if (err instanceof UndeterminablePortableType) return c;
2099
+ throw err;
2100
+ }
2101
+ });
2102
+ assertPortableComponentGraph({
2103
+ irVersion: 1,
2104
+ exprVersion: EXPR_VERSION,
2105
+ components: out
2106
+ });
2107
+ return out;
2108
+ }
2109
+
1879
2110
  // src/spec/operations.ts
1880
2111
  function paramSpecs(params) {
1881
2112
  const out = {};
@@ -2311,6 +2542,9 @@ function buildQuerySpec(def) {
2311
2542
  operations,
2312
2543
  // operation 'query' → a single entity object; 'list' → a connection.
2313
2544
  cardinality: def.operation === "query" ? "one" : "many",
2545
+ // The root model — the SSoT anchor for the bc#45 type-notation derivation
2546
+ // (build-internal; dropped by toPortableQuery, so operations.json is unchanged).
2547
+ entity: def.entity.name,
2314
2548
  ...executionPlan !== void 0 ? { executionPlan } : {},
2315
2549
  // Pure-documentation description (issue #154); absent → byte-identical spec.
2316
2550
  ...def.description !== void 0 ? { description: def.description } : {}
@@ -2398,6 +2632,15 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
2398
2632
  },
2399
2633
  contractSpecs
2400
2634
  );
2635
+ const typeFacts = /* @__PURE__ */ new Map();
2636
+ for (const [name, spec] of Object.entries(querySpecs)) {
2637
+ if (spec.entity === void 0) continue;
2638
+ typeFacts.set(name, {
2639
+ rootEntity: spec.entity,
2640
+ cardinalityOne: spec.cardinality === "one"
2641
+ });
2642
+ }
2643
+ const annotatedComponents = typeFacts.size > 0 ? annotateComponents(components, typeFacts, buildManifest()) : components;
2401
2644
  const portableQueries = {};
2402
2645
  for (const [name, spec] of Object.entries(querySpecs)) {
2403
2646
  portableQueries[name] = toPortableQuery(name, spec);
@@ -2416,7 +2659,7 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
2416
2659
  return {
2417
2660
  version: operationsSpecVersion(content),
2418
2661
  ...content,
2419
- ...components.length > 0 ? { components } : {}
2662
+ ...annotatedComponents.length > 0 ? { components: annotatedComponents } : {}
2420
2663
  };
2421
2664
  }
2422
2665