simple-graph-query 2.5.0 → 2.5.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.
@@ -8,7 +8,7 @@ export type BinaryRelationExample = {
8
8
  pairs: Set<AtomPair>;
9
9
  datum: IDataInstance;
10
10
  };
11
- type ExpressionNode = {
11
+ export type ExpressionNode = {
12
12
  kind: "identifier";
13
13
  name: string;
14
14
  } | {
@@ -22,6 +22,41 @@ type ExpressionNode = {
22
22
  } | {
23
23
  kind: "closure";
24
24
  child: ExpressionNode;
25
+ } | {
26
+ kind: "reflexive-closure";
27
+ child: ExpressionNode;
28
+ } | {
29
+ kind: "transpose";
30
+ child: ExpressionNode;
31
+ } | {
32
+ kind: "comprehension";
33
+ varName: string;
34
+ domain: ExpressionNode;
35
+ body: ExpressionNode;
36
+ } | {
37
+ kind: "all" | "some" | "no" | "one" | "lone";
38
+ varName: string;
39
+ domain: ExpressionNode;
40
+ body: ExpressionNode;
41
+ } | {
42
+ kind: "and" | "or" | "implies" | "iff";
43
+ left: ExpressionNode;
44
+ right: ExpressionNode;
45
+ } | {
46
+ kind: "not";
47
+ child: ExpressionNode;
48
+ } | {
49
+ kind: "in" | "eq" | "neq";
50
+ left: ExpressionNode;
51
+ right: ExpressionNode;
52
+ } | {
53
+ kind: "lt" | "gt" | "lte" | "gte";
54
+ left: ExpressionNode;
55
+ right: ExpressionNode;
56
+ } | {
57
+ kind: "box-join";
58
+ base: ExpressionNode;
59
+ args: ExpressionNode[];
25
60
  };
26
61
  export type WhyNode = {
27
62
  kind: ExpressionNode["kind"];
@@ -45,4 +80,3 @@ export type SynthesisWhy = {
45
80
  };
46
81
  export declare function synthesizeSelectorWithWhy(examples: AtomSelectionExample[], maxDepth?: number): SynthesisWhy;
47
82
  export declare function synthesizeBinaryRelationWithWhy(examples: BinaryRelationExample[], maxDepth?: number): SynthesisWhy;
48
- export {};
@@ -50955,10 +50955,20 @@ function nodeToString(node) {
50955
50955
  switch (node.kind) {
50956
50956
  case "identifier":
50957
50957
  return node.name;
50958
+ // Unary relational operators
50958
50959
  case "closure": {
50959
50960
  const inner = nodeToString(node.child);
50960
50961
  return `^${wrapForPrefix(inner)}`;
50961
50962
  }
50963
+ case "reflexive-closure": {
50964
+ const inner = nodeToString(node.child);
50965
+ return `*${wrapForPrefix(inner)}`;
50966
+ }
50967
+ case "transpose": {
50968
+ const inner = nodeToString(node.child);
50969
+ return `~${wrapForPrefix(inner)}`;
50970
+ }
50971
+ // Binary relational operators
50962
50972
  case "join": {
50963
50973
  const left = wrapForJoin(node.left);
50964
50974
  const right = wrapForJoin(node.right);
@@ -50970,10 +50980,59 @@ function nodeToString(node) {
50970
50980
  return `(${nodeToString(node.left)} & ${nodeToString(node.right)})`;
50971
50981
  case "difference":
50972
50982
  return `(${nodeToString(node.left)} - ${nodeToString(node.right)})`;
50983
+ // Set comprehension
50984
+ case "comprehension":
50985
+ return `{${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)}}`;
50986
+ // Quantified expressions
50987
+ case "all":
50988
+ return `(all ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
50989
+ case "some":
50990
+ return `(some ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
50991
+ case "no":
50992
+ return `(no ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
50993
+ case "one":
50994
+ return `(one ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
50995
+ case "lone":
50996
+ return `(lone ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
50997
+ // Logical operators
50998
+ case "and":
50999
+ return `(${nodeToString(node.left)} and ${nodeToString(node.right)})`;
51000
+ case "or":
51001
+ return `(${nodeToString(node.left)} or ${nodeToString(node.right)})`;
51002
+ case "implies":
51003
+ return `(${nodeToString(node.left)} => ${nodeToString(node.right)})`;
51004
+ case "iff":
51005
+ return `(${nodeToString(node.left)} <=> ${nodeToString(node.right)})`;
51006
+ case "not":
51007
+ return `!${wrapForPrefix(nodeToString(node.child))}`;
51008
+ // Relational comparison
51009
+ case "in":
51010
+ return `(${nodeToString(node.left)} in ${nodeToString(node.right)})`;
51011
+ case "eq":
51012
+ return `(${nodeToString(node.left)} = ${nodeToString(node.right)})`;
51013
+ case "neq":
51014
+ return `(${nodeToString(node.left)} != ${nodeToString(node.right)})`;
51015
+ // Numeric comparison
51016
+ case "lt":
51017
+ return `(${nodeToString(node.left)} < ${nodeToString(node.right)})`;
51018
+ case "gt":
51019
+ return `(${nodeToString(node.left)} > ${nodeToString(node.right)})`;
51020
+ case "lte":
51021
+ return `(${nodeToString(node.left)} <= ${nodeToString(node.right)})`;
51022
+ case "gte":
51023
+ return `(${nodeToString(node.left)} >= ${nodeToString(node.right)})`;
51024
+ // Box join
51025
+ case "box-join":
51026
+ return `${nodeToString(node.base)}[${node.args.map(nodeToString).join(", ")}]`;
50973
51027
  }
50974
51028
  }
50975
51029
  function wrapForJoin(node) {
50976
- if (node.kind === "identifier" || node.kind === "closure") {
51030
+ // Identifiers and prefix unary operators don't need wrapping
51031
+ if (node.kind === "identifier" ||
51032
+ node.kind === "closure" ||
51033
+ node.kind === "reflexive-closure" ||
51034
+ node.kind === "transpose" ||
51035
+ node.kind === "box-join") {
50977
51036
  return nodeToString(node);
50978
51037
  }
50979
51038
  return `(${nodeToString(node)})`;
@@ -50997,10 +51056,16 @@ function normalizeUnaryResult(result) {
50997
51056
  return null;
50998
51057
  }
50999
51058
  const value = tuple[0];
51000
- if (typeof value !== "string") {
51059
+ // Accept strings or numbers (integers are often returned as numbers)
51060
+ if (typeof value === "string") {
51061
+ ids.add(value);
51062
+ }
51063
+ else if (typeof value === "number") {
51064
+ ids.add(String(value));
51065
+ }
51066
+ else {
51001
51067
  return null;
51002
51068
  }
51003
- ids.add(value);
51004
51069
  }
51005
51070
  return ids;
51006
51071
  }
@@ -51015,10 +51080,13 @@ function normalizeBinaryResult(result) {
51015
51080
  return null;
51016
51081
  }
51017
51082
  const [first, second] = tuple;
51018
- if (typeof first !== "string" || typeof second !== "string") {
51083
+ // Accept strings or numbers for both elements
51084
+ const firstStr = typeof first === "string" ? first : typeof first === "number" ? String(first) : null;
51085
+ const secondStr = typeof second === "string" ? second : typeof second === "number" ? String(second) : null;
51086
+ if (firstStr === null || secondStr === null) {
51019
51087
  return null;
51020
51088
  }
51021
- ids.add(`${first}\u0000${second}`);
51089
+ ids.add(`${firstStr}\u0000${secondStr}`);
51022
51090
  }
51023
51091
  return ids;
51024
51092
  }
@@ -51068,6 +51136,99 @@ function classifyIdentifier(name, datums) {
51068
51136
  }
51069
51137
  return "other";
51070
51138
  }
51139
+ // Build semantically meaningful join candidates based on type-relation compatibility.
51140
+ // For a relation with types [T1, T2, ...], we generate joins like T1.relation
51141
+ // which projects the relation to its range. This helps synthesize expressions
51142
+ // like "Node.key" when selecting integer values that are keys of nodes.
51143
+ function buildSemanticJoinCandidates(datums) {
51144
+ const candidates = [];
51145
+ const seenExpressions = new Set();
51146
+ // Get relations and types that exist across all datums
51147
+ const sharedRelationNames = new Set();
51148
+ const sharedTypeIds = new Set();
51149
+ if (datums.length === 0)
51150
+ return candidates;
51151
+ // Initialize with first datum
51152
+ const firstDatum = datums[0];
51153
+ firstDatum.getRelations().forEach((r) => sharedRelationNames.add(r.name));
51154
+ firstDatum.getTypes().forEach((t) => sharedTypeIds.add(t.id));
51155
+ // Intersect with remaining datums
51156
+ for (let i = 1; i < datums.length; i++) {
51157
+ const datum = datums[i];
51158
+ const datumRelations = new Set(datum.getRelations().map((r) => r.name));
51159
+ const datumTypes = new Set(datum.getTypes().map((t) => t.id));
51160
+ for (const name of sharedRelationNames) {
51161
+ if (!datumRelations.has(name))
51162
+ sharedRelationNames.delete(name);
51163
+ }
51164
+ for (const id of sharedTypeIds) {
51165
+ if (!datumTypes.has(id))
51166
+ sharedTypeIds.delete(id);
51167
+ }
51168
+ }
51169
+ // For each shared relation, check if its domain type is also shared
51170
+ // and create Type.relation join candidates
51171
+ for (const relationName of sharedRelationNames) {
51172
+ // Get the relation's type signature from the first datum (should be consistent)
51173
+ const relation = firstDatum.getRelations().find((r) => r.name === relationName);
51174
+ if (!relation || relation.types.length < 2)
51175
+ continue;
51176
+ const domainType = relation.types[0];
51177
+ // Check if the domain type (or a compatible type) is available
51178
+ // We look for types in the hierarchy that could be the domain
51179
+ for (const typeId of sharedTypeIds) {
51180
+ const typeObj = firstDatum.getTypes().find((t) => t.id === typeId);
51181
+ if (!typeObj)
51182
+ continue;
51183
+ // Check if this type is compatible with the relation's domain
51184
+ // Either the type itself matches, or it's in the type hierarchy
51185
+ const isCompatible = typeId === domainType ||
51186
+ typeObj.types.includes(domainType) ||
51187
+ // Also check if the domain type is a subtype of this type
51188
+ firstDatum.getTypes().find((t) => t.id === domainType)?.types.includes(typeId);
51189
+ if (isCompatible) {
51190
+ const joinNode = {
51191
+ kind: "join",
51192
+ left: { kind: "identifier", name: typeId },
51193
+ right: { kind: "identifier", name: relationName },
51194
+ };
51195
+ const exprString = nodeToString(joinNode);
51196
+ if (!seenExpressions.has(exprString)) {
51197
+ seenExpressions.add(exprString);
51198
+ candidates.push(joinNode);
51199
+ }
51200
+ }
51201
+ }
51202
+ }
51203
+ // Also add relation.Type joins for inverse projections (getting domain values)
51204
+ for (const relationName of sharedRelationNames) {
51205
+ const relation = firstDatum.getRelations().find((r) => r.name === relationName);
51206
+ if (!relation || relation.types.length < 2)
51207
+ continue;
51208
+ const rangeType = relation.types[relation.types.length - 1];
51209
+ for (const typeId of sharedTypeIds) {
51210
+ const typeObj = firstDatum.getTypes().find((t) => t.id === typeId);
51211
+ if (!typeObj)
51212
+ continue;
51213
+ const isCompatible = typeId === rangeType ||
51214
+ typeObj.types.includes(rangeType) ||
51215
+ firstDatum.getTypes().find((t) => t.id === rangeType)?.types.includes(typeId);
51216
+ if (isCompatible) {
51217
+ const joinNode = {
51218
+ kind: "join",
51219
+ left: { kind: "identifier", name: relationName },
51220
+ right: { kind: "identifier", name: typeId },
51221
+ };
51222
+ const exprString = nodeToString(joinNode);
51223
+ if (!seenExpressions.has(exprString)) {
51224
+ seenExpressions.add(exprString);
51225
+ candidates.push(joinNode);
51226
+ }
51227
+ }
51228
+ }
51229
+ }
51230
+ return candidates;
51231
+ }
51071
51232
  function buildBaseNodes(datums) {
51072
51233
  const baseNames = intersectNames(datums);
51073
51234
  // Always include standard top-level identifiers when present in the language
@@ -51125,11 +51286,20 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51125
51286
  if (baseNodes.length === 0) {
51126
51287
  throw new SelectorSynthesisError("No shared identifiers available across provided data instances");
51127
51288
  }
51289
+ // Check base identifiers first
51128
51290
  for (const node of baseNodes) {
51129
51291
  if (matchesTargets(node, evaluatedExamples, normalizer)) {
51130
51292
  return node;
51131
51293
  }
51132
51294
  }
51295
+ // Check semantic join candidates early - these are type-aware joins like Node.key
51296
+ // that are likely to be what the user wants when selecting relation ranges/domains
51297
+ const semanticJoins = buildSemanticJoinCandidates(datums);
51298
+ for (const node of semanticJoins) {
51299
+ if (matchesTargets(node, evaluatedExamples, normalizer)) {
51300
+ return node;
51301
+ }
51302
+ }
51133
51303
  const queue = [];
51134
51304
  const queued = new Set();
51135
51305
  const visited = new Set();
@@ -51141,6 +51311,10 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51141
51311
  queue.push({ node, depth });
51142
51312
  queued.add(key);
51143
51313
  };
51314
+ // Mark semantic joins as already visited so we don't re-check them
51315
+ for (const node of semanticJoins) {
51316
+ visited.add(nodeToString(node));
51317
+ }
51144
51318
  baseNodes.forEach((node) => enqueue(node, 0));
51145
51319
  while (queue.length > 0) {
51146
51320
  const current = queue.shift();
@@ -51156,18 +51330,69 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51156
51330
  if (current.depth >= maxDepth) {
51157
51331
  continue;
51158
51332
  }
51159
- // Unary expansions
51333
+ // Unary relational expansions
51160
51334
  enqueue({ kind: "closure", child: current.node }, current.depth + 1);
51335
+ enqueue({ kind: "reflexive-closure", child: current.node }, current.depth + 1);
51336
+ enqueue({ kind: "transpose", child: current.node }, current.depth + 1);
51161
51337
  for (const other of combinationPool) {
51162
51338
  const leftKey = nodeToString(current.node);
51163
51339
  const rightKey = nodeToString(other);
51340
+ // Set operations (commutative, so canonicalize order)
51164
51341
  const [unionLeft, unionRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51165
51342
  enqueue({ kind: "union", left: unionLeft, right: unionRight }, current.depth + 1);
51166
51343
  const [interLeft, interRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51167
51344
  enqueue({ kind: "intersection", left: interLeft, right: interRight }, current.depth + 1);
51345
+ // Difference is not commutative
51346
+ enqueue({ kind: "difference", left: current.node, right: other }, current.depth + 1);
51347
+ if (leftKey !== rightKey) {
51348
+ enqueue({ kind: "difference", left: other, right: current.node }, current.depth + 1);
51349
+ }
51350
+ // Join is not commutative
51168
51351
  enqueue({ kind: "join", left: current.node, right: other }, current.depth + 1);
51169
51352
  enqueue({ kind: "join", left: other, right: current.node }, current.depth + 1);
51170
51353
  }
51354
+ // Generate set comprehensions: {v: domain | body}
51355
+ // For each type, try comprehensions with simple membership conditions
51356
+ // This allows synthesizing expressions like {n: Node | n.key in SomeSet}
51357
+ if (current.depth + 2 <= maxDepth) {
51358
+ for (const domainNode of combinationPool) {
51359
+ // Only use types as comprehension domains
51360
+ if (domainNode.kind !== "identifier")
51361
+ continue;
51362
+ const domainName = domainNode.name;
51363
+ const classification = classifyIdentifier(domainName, datums);
51364
+ if (classification !== "type")
51365
+ continue;
51366
+ const varName = "v"; // Use a simple variable name
51367
+ // Try: {v: Domain | v in current.node}
51368
+ const varNode = { kind: "identifier", name: varName };
51369
+ enqueue({
51370
+ kind: "comprehension",
51371
+ varName,
51372
+ domain: domainNode,
51373
+ body: { kind: "in", left: varNode, right: current.node },
51374
+ }, current.depth + 2);
51375
+ // Try: {v: Domain | v.relation in SomeSet} for relations
51376
+ for (const relNode of combinationPool) {
51377
+ if (relNode.kind !== "identifier")
51378
+ continue;
51379
+ if (classifyIdentifier(relNode.name, datums) !== "relation")
51380
+ continue;
51381
+ const joinExpr = {
51382
+ kind: "join",
51383
+ left: varNode,
51384
+ right: relNode,
51385
+ };
51386
+ // {v: Domain | v.relation in current.node}
51387
+ enqueue({
51388
+ kind: "comprehension",
51389
+ varName,
51390
+ domain: domainNode,
51391
+ body: { kind: "in", left: joinExpr, right: current.node },
51392
+ }, current.depth + 2);
51393
+ }
51394
+ }
51395
+ }
51171
51396
  }
51172
51397
  throw new SelectorSynthesisError("Unable to synthesize an expression matching all examples");
51173
51398
  }
@@ -51195,14 +51420,31 @@ function buildWhyNode(node, evaluator, normalizer) {
51195
51420
  result,
51196
51421
  };
51197
51422
  switch (node.kind) {
51423
+ // Leaves
51198
51424
  case "identifier":
51199
51425
  return base;
51426
+ // Unary operators
51200
51427
  case "closure":
51428
+ case "reflexive-closure":
51429
+ case "transpose":
51430
+ case "not":
51201
51431
  return { ...base, children: [buildWhyNode(node.child, evaluator, normalizer)] };
51432
+ // Binary operators
51202
51433
  case "join":
51203
51434
  case "union":
51204
51435
  case "intersection":
51205
51436
  case "difference":
51437
+ case "and":
51438
+ case "or":
51439
+ case "implies":
51440
+ case "iff":
51441
+ case "in":
51442
+ case "eq":
51443
+ case "neq":
51444
+ case "lt":
51445
+ case "gt":
51446
+ case "lte":
51447
+ case "gte":
51206
51448
  return {
51207
51449
  ...base,
51208
51450
  children: [
@@ -51210,6 +51452,29 @@ function buildWhyNode(node, evaluator, normalizer) {
51210
51452
  buildWhyNode(node.right, evaluator, normalizer),
51211
51453
  ],
51212
51454
  };
51455
+ // Quantified and comprehension expressions
51456
+ case "all":
51457
+ case "some":
51458
+ case "no":
51459
+ case "one":
51460
+ case "lone":
51461
+ case "comprehension":
51462
+ return {
51463
+ ...base,
51464
+ children: [
51465
+ buildWhyNode(node.domain, evaluator, normalizer),
51466
+ buildWhyNode(node.body, evaluator, normalizer),
51467
+ ],
51468
+ };
51469
+ // Box join
51470
+ case "box-join":
51471
+ return {
51472
+ ...base,
51473
+ children: [
51474
+ buildWhyNode(node.base, evaluator, normalizer),
51475
+ ...node.args.map((arg) => buildWhyNode(arg, evaluator, normalizer)),
51476
+ ],
51477
+ };
51213
51478
  default:
51214
51479
  return base;
51215
51480
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-graph-query",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "TypeScript evaluator for Forge expressions with browser-compatible UMD bundle",
5
5
  "main": "dist/simple-graph-query.bundle.js",
6
6
  "types": "dist/index.d.ts",