factbag 0.0.0-alpha.0 → 0.0.0-alpha.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # factbag
2
2
 
3
+ ## 0.0.0-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ebc4a8a: A `letIn` or aggregate whose variable is already bound now constrains that variable instead of rebinding it. Before, a rule such as `tax(P, R) iff letIn(R, ...)` concluded `tax(alice, 99)` as true when its body computed 98, and an `or` branch evaluated under another branch's bindings reported those bindings as true. Proofs for such queries now carry a temporary variable and an `eq` comparison.
8
+ - eca65c2: An `or` is now judged per binding environment, so a branch that fails without binding a variable can no longer borrow the truth of a sibling branch whose bindings were discarded. Before, `and(or(p(X), r()), s(X))` could be judged true when `p(x1)` and `s(x3)` held but nothing satisfied both: the false `r()` branch produced an environment without `X`, the `or` was judged true there via `p(x1)`, and `s(X)` then ranged over every candidate. The recursion validator's edge collector also now carries variables bound in every `or` branch forward, matching the variable-use checker, so a comparison after such an `or` is no longer reported as unconstructive recursion.
9
+
3
10
  ## 0.0.0-alpha.0
4
11
 
5
12
  Initial experimental distribution of core, corpus, guards, and Jev functionality.
package/README.md CHANGED
@@ -7,8 +7,8 @@ sources and rules behind a conclusion.
7
7
  Factbag includes a document corpus, permission controls, and an integration with
8
8
  TypeSafe AI’s Jev model.
9
9
 
10
- Read [Introducing Factbag](https://blog.du-pless.is/introducing-factbag) for how and
11
- why to use it, or [Jev and the Art of Truth Maintenance](https://blog.du-pless.is/jev-and-the-art-of-truth-maintenance)
10
+ Read [Introducing Factbag](https://daniel.du-pless.is/introducing-factbag) for how and
11
+ why to use it, or [Jev and the Art of Truth Maintenance](https://daniel.du-pless.is/jev-and-the-art-of-truth-maintenance)
12
12
  for the thinking behind it.
13
13
 
14
14
  ## Install
@@ -79,6 +79,15 @@ function uniqueVariables(variables) {
79
79
  }
80
80
  return unique;
81
81
  }
82
+ function bindingsAfterOr(branchBindings, bound) {
83
+ const next = new Set(bound);
84
+ for (const candidate of uniqueVariables(branchBindings.flatMap((branch) => [...branch]))) {
85
+ if (branchBindings.every((branch) => branch.has(candidate))) {
86
+ next.add(candidate);
87
+ }
88
+ }
89
+ return next;
90
+ }
82
91
  function variablesIn(value) {
83
92
  if (isVar(value)) {
84
93
  return [value];
@@ -95,7 +104,7 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
95
104
  if (isAtomLike(body)) {
96
105
  edges.push({ from: headPredicate, to: body.predicate, kind: "positive" });
97
106
  bindVariables(body, bound);
98
- return;
107
+ return bound;
99
108
  }
100
109
  if (isNegativeLiteral(body)) {
101
110
  edges.push({
@@ -103,20 +112,26 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
103
112
  to: body.atom.predicate,
104
113
  kind: body.atom.predicate.world === "closed" ? "closed-world-not" : "explicit-not"
105
114
  });
106
- bindVariables(body.atom, bound);
107
- return;
115
+ return bound;
108
116
  }
109
117
  if (isFormula(body, "and")) {
118
+ let nextBound = bound;
110
119
  for (const arg of body.args) {
111
- collectBodyEdges(arg, headPredicate, bound, edges);
120
+ nextBound = collectBodyEdges(arg, headPredicate, nextBound, edges);
112
121
  }
113
- return;
122
+ return nextBound;
123
+ }
124
+ if (isFormula(body, "or")) {
125
+ const branchBindings = body.args.map(
126
+ (arg) => collectBodyEdges(arg, headPredicate, new Set(bound), edges)
127
+ );
128
+ return bindingsAfterOr(branchBindings, bound);
114
129
  }
115
- if (isFormula(body, "or") || isFormula(body, "exists")) {
130
+ if (isFormula(body, "exists")) {
116
131
  for (const arg of body.args) {
117
132
  collectBodyEdges(arg, headPredicate, new Set(bound), edges);
118
133
  }
119
- return;
134
+ return bound;
120
135
  }
121
136
  if (isFormula(body, "letIn")) {
122
137
  const [variable, value, nextBody] = body.args;
@@ -128,12 +143,12 @@ function collectBodyEdges(body, headPredicate, bound, edges) {
128
143
  if (isVar(variable)) {
129
144
  bound.add(variable);
130
145
  }
131
- collectBodyEdges(nextBody, headPredicate, bound, edges);
132
- return;
146
+ return collectBodyEdges(nextBody, headPredicate, bound, edges);
133
147
  }
134
148
  if (isPrimitiveFormula(body) && hasUnboundVariable(body, bound)) {
135
149
  edges.push({ from: headPredicate, to: headPredicate, kind: "primitive-ungrounded" });
136
150
  }
151
+ return bound;
137
152
  }
138
153
  function collectAggregateEdges(aggregate, headPredicate, bound, edges) {
139
154
  for (const arg of aggregate.args.slice(1)) {
@@ -252,7 +267,10 @@ function assertSafeBody(body, bound) {
252
267
  return nextBound;
253
268
  }
254
269
  if (isFormula(body, "or")) {
255
- return bindingsAvailableAfterOr(body.args, bound);
270
+ return bindingsAfterOr(
271
+ body.args.map((arg) => assertSafeBody(arg, new Set(bound))),
272
+ bound
273
+ );
256
274
  }
257
275
  if (isFormula(body, "exists")) {
258
276
  const nextBody = body.args.at(-1);
@@ -288,16 +306,6 @@ function assertSafeAggregate(aggregate, bound) {
288
306
  assertSafeBody(options.completeWhen, new Set(bound));
289
307
  }
290
308
  }
291
- function bindingsAvailableAfterOr(args, bound) {
292
- const branchBindings = args.map((arg) => assertSafeBody(arg, new Set(bound)));
293
- const next = new Set(bound);
294
- for (const candidate of uniqueVariables(branchBindings.flatMap((branch) => [...branch]))) {
295
- if (branchBindings.length > 0 && branchBindings.every((branch) => branch.has(candidate))) {
296
- next.add(candidate);
297
- }
298
- }
299
- return next;
300
- }
301
309
  function assertVariablesBound(variables, bound) {
302
310
  const unbound = uniqueVariables(variables).filter((variable) => !bound.has(variable));
303
311
  if (unbound.length > 0) {
@@ -727,7 +735,15 @@ function assertNever(value, message) {
727
735
  }
728
736
  function evaluateRuleBodyMany(literals, rules, body, bindings, active, context, pathCanHaveUnavailableBindings = false) {
729
737
  if (isAtomLike(body)) {
730
- return evaluateAtomBody(literals, rules, erasedAtom(body), bindings, active, context);
738
+ return evaluateAtomBody(
739
+ literals,
740
+ rules,
741
+ erasedAtom(body),
742
+ bindings,
743
+ active,
744
+ context,
745
+ pathCanHaveUnavailableBindings
746
+ );
731
747
  }
732
748
  if (isLiteralLike(body) && body.sign === "-") {
733
749
  const bodyAtom = erasedAtom(body.atom);
@@ -907,9 +923,13 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
907
923
  )
908
924
  );
909
925
  const environments = candidateBindings.length === 0 ? [bindings] : candidateBindings;
910
- return environments.map((environment) => {
911
- const judgments = args.map((arg) => {
912
- const results = evaluateRuleBodyMany(
926
+ const queue = [...environments];
927
+ const seen = new Set(queue.map(bindingsKey));
928
+ const results = [];
929
+ for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
930
+ const environment = next;
931
+ const judgments = args.flatMap((arg) => {
932
+ const branch = evaluateRuleBodyMany(
913
933
  literals,
914
934
  rules,
915
935
  arg,
@@ -918,15 +938,27 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
918
938
  context,
919
939
  pathCanHaveUnavailableBindings
920
940
  );
921
- assert(results.length > 0, "Or branch evaluation produced no results");
922
- const result = bestResult(results);
923
- assert(result !== void 0, "Or branch evaluation produced no best result");
924
- return result.judgment;
941
+ for (const result2 of branch) {
942
+ if (bindsBeyond(result2.bindings, environment)) {
943
+ const key = bindingsKey(result2.bindings);
944
+ if (!seen.has(key)) {
945
+ seen.add(key);
946
+ queue.push(result2.bindings);
947
+ }
948
+ }
949
+ }
950
+ const result = bestResult(
951
+ branch.filter((candidate) => !bindsBeyond(candidate.bindings, environment))
952
+ );
953
+ return result === void 0 ? [] : [result.judgment];
925
954
  });
955
+ if (judgments.length === 0) {
956
+ continue;
957
+ }
926
958
  const judgmentBindings = bindingsArray(environment);
927
959
  const reboundJudgments = judgments.map((judgment) => withBindings(judgment, judgmentBindings));
928
960
  const goal = { kind: "or", goals: reboundJudgments.map((judgment) => judgment.goal) };
929
- return {
961
+ results.push({
930
962
  judgment: compoundJudgment(
931
963
  goal,
932
964
  maxGoalValue(...reboundJudgments.map((judgment) => judgment.value)),
@@ -934,8 +966,9 @@ function evaluateOrBody(literals, rules, args, bindings, active, context, pathCa
934
966
  reboundJudgments
935
967
  ),
936
968
  bindings: environment
937
- };
938
- });
969
+ });
970
+ }
971
+ return results;
939
972
  }
940
973
  function evaluateAndBody(literals, rules, args, bindings, active, context, pathCanHaveUnavailableBindings) {
941
974
  let partials = [{ bindings, judgments: [] }];
@@ -976,7 +1009,7 @@ function evaluateAndBody(literals, rules, args, bindings, active, context, pathC
976
1009
  };
977
1010
  });
978
1011
  }
979
- function evaluateAtomBody(literals, rules, atom, bindings, active, context) {
1012
+ function evaluateAtomBody(literals, rules, atom, bindings, active, context, pathCanHaveUnavailableBindings = false) {
980
1013
  const evaluateGrounded = (environment) => {
981
1014
  const grounded = groundAtom(atom, environment);
982
1015
  const judgmentBindings = bindingsArray(environment);
@@ -998,7 +1031,14 @@ function evaluateAtomBody(literals, rules, atom, bindings, active, context) {
998
1031
  ])
999
1032
  )
1000
1033
  };
1001
- const candidateBindings = candidateGroundAtoms(literals, rules, pattern, active, context.search).map((candidate) => extendBindings(atom, candidate, bindings)).filter((candidate) => candidate !== void 0);
1034
+ const candidateBindings = candidateGroundAtoms(
1035
+ literals,
1036
+ rules,
1037
+ pattern,
1038
+ active,
1039
+ context.search,
1040
+ pathCanHaveUnavailableBindings
1041
+ ).map((candidate) => extendBindings(atom, candidate, bindings)).filter((candidate) => candidate !== void 0);
1002
1042
  if (candidateBindings.length === 0) {
1003
1043
  if (atom.predicate.world === "closed") {
1004
1044
  assert(!canGroundAtom(atom, bindings), "Ground closed-world absence must use ground solving");
@@ -1181,7 +1221,7 @@ function evaluateExpression(expression, bindings, context) {
1181
1221
  const right = evaluateExpression(expression.args[1], left.bindings, context);
1182
1222
  assert(isScalarGoalExpression(left.expression), "Left arithmetic operand must be scalar");
1183
1223
  assert(isScalarGoalExpression(right.expression), "Right arithmetic operand must be scalar");
1184
- const variable = new Var(`$tmp${context.nextTemp++}`);
1224
+ const variable = new Var(`${TEMPORARY_PREFIX}${context.nextTemp++}`);
1185
1225
  const goal = {
1186
1226
  kind: expression.kind,
1187
1227
  bind: variable,
@@ -1220,6 +1260,10 @@ function evaluateExpression(expression, bindings, context) {
1220
1260
  };
1221
1261
  }
1222
1262
  function evaluateExpressionInto(variable, expression, bindings, context) {
1263
+ const existing = valueForVariable(bindings, variable);
1264
+ if (existing !== UNBOUND) {
1265
+ return constrainBoundVariable(variable, existing, expression, bindings, context);
1266
+ }
1223
1267
  if (isArithmeticFormula(expression)) {
1224
1268
  const left = evaluateExpression(expression.args[0], bindings, context);
1225
1269
  const right = evaluateExpression(expression.args[1], left.bindings, context);
@@ -1256,9 +1300,35 @@ function evaluateExpressionInto(variable, expression, bindings, context) {
1256
1300
  }
1257
1301
  const value = isVar(expression) ? valueForVariable(bindings, expression) : definedValue(expression, "Expression values must be defined");
1258
1302
  const nextBindings = new Map(bindings);
1259
- nextBindings.set(variable, value);
1303
+ if (value !== UNBOUND) {
1304
+ nextBindings.set(variable, value);
1305
+ }
1260
1306
  return { value, expression: variable, bindings: nextBindings, judgments: [] };
1261
1307
  }
1308
+ function constrainBoundVariable(variable, existing, expression, bindings, context) {
1309
+ const computed = evaluateExpression(expression, bindings, context);
1310
+ assert(
1311
+ isScalarGoalExpression(computed.expression),
1312
+ "A let binding of a bound variable must compare scalar values"
1313
+ );
1314
+ const goal = { kind: "eq", left: variable, right: computed.expression };
1315
+ const value = computed.value === UNBOUND ? "unknown" : compareValues("eq", existing, computed.value);
1316
+ const comparison = primitiveComparisonJudgment(
1317
+ goal,
1318
+ value,
1319
+ bindingsArray(computed.bindings),
1320
+ existing,
1321
+ variable,
1322
+ computed.value,
1323
+ computed.expression
1324
+ );
1325
+ return {
1326
+ value: existing,
1327
+ expression: variable,
1328
+ bindings: computed.bindings,
1329
+ judgments: [...computed.judgments, comparison]
1330
+ };
1331
+ }
1262
1332
  function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, active, context) {
1263
1333
  const [valueExpression, whereBody, options] = aggregate.args;
1264
1334
  const completeWhen = options?.completeWhen;
@@ -1282,11 +1352,11 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1282
1352
  assert(completeness !== void 0, "Aggregate completeness evaluation produced no best result");
1283
1353
  if (completeness.judgment.value !== "true") {
1284
1354
  const judgmentBindings = bindingsArray(bindings);
1285
- let goal;
1355
+ let goal2;
1286
1356
  switch (aggregate.kind) {
1287
1357
  case "sum":
1288
1358
  assert(isNumericGoalExpression(valueExpression), "Sum value must be numeric");
1289
- goal = {
1359
+ goal2 = {
1290
1360
  kind: "sum",
1291
1361
  bind: variable,
1292
1362
  value: valueExpression,
@@ -1295,7 +1365,7 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1295
1365
  };
1296
1366
  break;
1297
1367
  case "count":
1298
- goal = {
1368
+ goal2 = {
1299
1369
  kind: "count",
1300
1370
  bind: variable,
1301
1371
  where: whereGoal,
@@ -1308,12 +1378,14 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1308
1378
  return {
1309
1379
  kind: "incomplete",
1310
1380
  result: {
1311
- judgment: { value: "unknown", goal, bindings: judgmentBindings },
1381
+ judgment: { value: "unknown", goal: goal2, bindings: judgmentBindings },
1312
1382
  bindings
1313
1383
  }
1314
1384
  };
1315
1385
  }
1316
1386
  }
1387
+ const existing = valueForVariable(bindings, variable);
1388
+ const target = existing === UNBOUND ? variable : new Var(`$tmp${context.nextTemp++}`);
1317
1389
  const nextBindings = new Map(bindings);
1318
1390
  const values = rows.map((row) => resolveAggregateValue(valueExpression, row.bindings));
1319
1391
  let value;
@@ -1322,29 +1394,29 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1322
1394
  case "sum":
1323
1395
  assert(isNumericGoalExpression(valueExpression), "Sum value must be numeric");
1324
1396
  value = sumValues(values);
1325
- nextBindings.set(variable, value);
1397
+ nextBindings.set(target, value);
1326
1398
  judgment = primitiveSumJudgment(
1327
1399
  {
1328
1400
  kind: "sum",
1329
- bind: variable,
1401
+ bind: target,
1330
1402
  value: valueExpression,
1331
1403
  where: whereGoal
1332
1404
  },
1333
1405
  bindingsArray(nextBindings),
1334
1406
  values,
1335
- variable,
1407
+ target,
1336
1408
  value
1337
1409
  );
1338
1410
  break;
1339
1411
  case "count": {
1340
1412
  const range = countRange(possibleRows, isOpenAggregate(whereBody));
1341
1413
  value = exactCountValue(range);
1342
- nextBindings.set(variable, value);
1414
+ nextBindings.set(target, value);
1343
1415
  judgment = primitiveCountJudgment(
1344
- { kind: "count", bind: variable, where: whereGoal },
1416
+ { kind: "count", bind: target, where: whereGoal },
1345
1417
  bindingsArray(nextBindings),
1346
1418
  range,
1347
- variable,
1419
+ target,
1348
1420
  value
1349
1421
  );
1350
1422
  break;
@@ -1352,9 +1424,30 @@ function evaluateAggregateInto(literals, rules, variable, aggregate, bindings, a
1352
1424
  default:
1353
1425
  return assertNever(aggregate.kind, "Unhandled aggregate kind");
1354
1426
  }
1427
+ if (existing === UNBOUND) {
1428
+ return {
1429
+ kind: "complete",
1430
+ expression: { value, expression: variable, bindings: nextBindings, judgments: [judgment] }
1431
+ };
1432
+ }
1433
+ const goal = { kind: "eq", left: variable, right: target };
1434
+ const comparison = primitiveComparisonJudgment(
1435
+ goal,
1436
+ compareValues("eq", existing, value),
1437
+ bindingsArray(nextBindings),
1438
+ existing,
1439
+ variable,
1440
+ value,
1441
+ target
1442
+ );
1355
1443
  return {
1356
1444
  kind: "complete",
1357
- expression: { value, expression: variable, bindings: nextBindings, judgments: [judgment] }
1445
+ expression: {
1446
+ value: existing,
1447
+ expression: variable,
1448
+ bindings: nextBindings,
1449
+ judgments: [judgment, comparison]
1450
+ }
1358
1451
  };
1359
1452
  }
1360
1453
  function resolveAggregateValue(expression, bindings) {
@@ -1810,13 +1903,28 @@ function ruleContribution(kind, value) {
1810
1903
  function bindingsArray(bindings) {
1811
1904
  return [...bindings.entries()];
1812
1905
  }
1906
+ var TEMPORARY_PREFIX = "$tmp";
1907
+ function isTemporary(variable) {
1908
+ return variable.name.startsWith(TEMPORARY_PREFIX);
1909
+ }
1910
+ function bindsBeyond(result, environment) {
1911
+ for (const variable of result.keys()) {
1912
+ if (!isTemporary(variable) && !environment.has(variable)) {
1913
+ return true;
1914
+ }
1915
+ }
1916
+ return false;
1917
+ }
1918
+ function bindingsKey(binding) {
1919
+ return JSON.stringify(
1920
+ bindingsArray(binding).filter(([variable]) => !isTemporary(variable)).map(([variable, value]) => [variable.name, fieldIdentityToken(value)])
1921
+ );
1922
+ }
1813
1923
  function uniqueBindings(bindings) {
1814
1924
  const seen = /* @__PURE__ */ new Set();
1815
1925
  const unique = [];
1816
1926
  for (const binding of bindings) {
1817
- const key = JSON.stringify(
1818
- bindingsArray(binding).map(([variable, value]) => [variable.name, fieldIdentityToken(value)])
1819
- );
1927
+ const key = bindingsKey(binding);
1820
1928
  if (!seen.has(key)) {
1821
1929
  seen.add(key);
1822
1930
  unique.push(binding);
@@ -1831,16 +1939,16 @@ function groundAtomKey(atom) {
1831
1939
  const fields = atom.predicate.paramNames.map((name) => fieldIdentityToken(atom.fields[name]));
1832
1940
  return `${atom.predicate.name}:${JSON.stringify(fields)}`;
1833
1941
  }
1834
- function candidateGroundAtoms(literals, rules, atom, active, search) {
1942
+ function candidateGroundAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
1835
1943
  return search.candidates.candidates(
1836
1944
  atom,
1837
1945
  matchingGroundAtoms(literals, atom),
1838
- () => deriveCandidateAtoms(literals, rules, atom, active, search)
1946
+ () => deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings)
1839
1947
  ).filter(
1840
1948
  (candidate) => isAtomForPredicate(candidate, atom.predicate)
1841
1949
  );
1842
1950
  }
1843
- function deriveCandidateAtoms(literals, rules, atom, active, search) {
1951
+ function deriveCandidateAtoms(literals, rules, atom, active, search, pathCanHaveUnavailableBindings = false) {
1844
1952
  const atoms = [];
1845
1953
  for (const rule of rules) {
1846
1954
  const ruleHead = atomForPredicate(rule.head.atom, atom.predicate);
@@ -1852,9 +1960,17 @@ function deriveCandidateAtoms(literals, rules, atom, active, search) {
1852
1960
  continue;
1853
1961
  }
1854
1962
  const context = { nextTemp: 0, search, candidatePredicate: atom.predicate };
1855
- const bodies = evaluateRuleBodyMany(literals, rules, rule.body, bindings, active, context);
1963
+ const bodies = evaluateRuleBodyMany(
1964
+ literals,
1965
+ rules,
1966
+ rule.body,
1967
+ bindings,
1968
+ active,
1969
+ context,
1970
+ pathCanHaveUnavailableBindings
1971
+ );
1856
1972
  for (const body of bodies) {
1857
- if (!canGroundAtom(ruleHead, body.bindings)) {
1973
+ if (body.judgment.value === "false" || !canGroundAtom(ruleHead, body.bindings)) {
1858
1974
  continue;
1859
1975
  }
1860
1976
  const candidate = groundAtom(ruleHead, body.bindings);
@@ -23,5 +23,7 @@ export declare function isCompleteWhenOption(value: unknown): value is Readonly<
23
23
  export declare function isPrimitiveFormula(value: unknown): boolean;
24
24
  export declare function bindVariables(value: unknown, bound: Set<Var>): void;
25
25
  export declare function uniqueVariables(variables: readonly Var[]): Var[];
26
+ /** Bindings available after an `or`: those held before it plus those every branch makes. */
27
+ export declare function bindingsAfterOr(branchBindings: readonly ReadonlySet<Var>[], bound: ReadonlySet<Var>): Set<Var>;
26
28
  export declare function variablesIn(value: unknown): Var[];
27
29
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factbag",
3
- "version": "0.0.0-alpha.0",
3
+ "version": "0.0.0-alpha.1",
4
4
  "keywords": [
5
5
  "logic",
6
6
  "rules-engine",