@proposit/proposit-core 3.4.2 → 4.0.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/README.md +12 -6
- package/dist/cli/commands/analysis.d.ts.map +1 -1
- package/dist/cli/commands/analysis.js +7 -9
- package/dist/cli/commands/analysis.js.map +1 -1
- package/dist/cli/commands/graph.d.ts +1 -1
- package/dist/cli/commands/graph.d.ts.map +1 -1
- package/dist/cli/commands/graph.js +21 -15
- package/dist/cli/commands/graph.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +22 -2
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +64 -11
- package/dist/lib/core/argument-engine.js.map +1 -1
- package/dist/lib/core/evaluation/argument-evaluation.d.ts +77 -14
- package/dist/lib/core/evaluation/argument-evaluation.d.ts.map +1 -1
- package/dist/lib/core/evaluation/argument-evaluation.js +402 -235
- package/dist/lib/core/evaluation/argument-evaluation.js.map +1 -1
- package/dist/lib/core/evaluation/belnap.d.ts +22 -0
- package/dist/lib/core/evaluation/belnap.d.ts.map +1 -0
- package/dist/lib/core/evaluation/belnap.js +86 -0
- package/dist/lib/core/evaluation/belnap.js.map +1 -0
- package/dist/lib/core/evaluation/premise-resolver.d.ts +13 -0
- package/dist/lib/core/evaluation/premise-resolver.d.ts.map +1 -0
- package/dist/lib/core/evaluation/premise-resolver.js +35 -0
- package/dist/lib/core/evaluation/premise-resolver.js.map +1 -0
- package/dist/lib/core/evaluation/satisfiability.d.ts +52 -0
- package/dist/lib/core/evaluation/satisfiability.d.ts.map +1 -0
- package/dist/lib/core/evaluation/satisfiability.js +212 -0
- package/dist/lib/core/evaluation/satisfiability.js.map +1 -0
- package/dist/lib/core/evaluation/validation.d.ts +4 -4
- package/dist/lib/core/evaluation/validation.d.ts.map +1 -1
- package/dist/lib/core/evaluation/validation.js +4 -4
- package/dist/lib/core/evaluation/validation.js.map +1 -1
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +46 -7
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +19 -9
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.d.ts +4 -3
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +14 -17
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/types/evaluation.d.ts +241 -35
- package/dist/lib/types/evaluation.d.ts.map +1 -1
- package/dist/lib/types/evaluation.js +13 -1
- package/dist/lib/types/evaluation.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/core/evaluation/grading.d.ts +0 -29
- package/dist/lib/core/evaluation/grading.d.ts.map +0 -1
- package/dist/lib/core/evaluation/grading.js +0 -50
- package/dist/lib/core/evaluation/grading.js.map +0 -1
- package/dist/lib/core/evaluation/kleene.d.ts +0 -12
- package/dist/lib/core/evaluation/kleene.d.ts.map +0 -1
- package/dist/lib/core/evaluation/kleene.js +0 -29
- package/dist/lib/core/evaluation/kleene.js.map +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { isClaimBound, isPremiseBound, } from "../../schemata/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { CONTESTED, } from "../../types/evaluation.js";
|
|
3
|
+
import { belnapAnd, belnapNot, belnapOr, belnapImplies, belnapIff, hasFalseComponent, hasTrueComponent, joinKnowledge, } from "./belnap.js";
|
|
4
|
+
import { createPremiseBoundResolver } from "./premise-resolver.js";
|
|
5
|
+
import { isPremiseSetSatisfiable } from "./satisfiability.js";
|
|
3
6
|
import { makeErrorIssue, makeValidationResult } from "./validation.js";
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
8
|
+
* Evaluate an expression subtree under a fixed variable assignment, using the
|
|
9
|
+
* four-valued Belnap connectives.
|
|
6
10
|
*
|
|
7
11
|
* Total and side-effect free: unknown/missing variables and empty operators
|
|
8
12
|
* yield `null`, and `formula` wrappers pass through to their single child.
|
|
@@ -13,79 +17,122 @@ import { makeErrorIssue, makeValidationResult } from "./validation.js";
|
|
|
13
17
|
* The operator base cases (`and` seeds `true`, `or` seeds `false`) match
|
|
14
18
|
* `propagateOperatorConstraints`' internal resolver so the two agree.
|
|
15
19
|
*/
|
|
16
|
-
export function
|
|
20
|
+
export function evaluateSubtree(rootExpressionId, getExpression, getChildren, variables) {
|
|
17
21
|
const expr = getExpression(rootExpressionId);
|
|
18
22
|
if (!expr)
|
|
19
23
|
return null;
|
|
20
24
|
if (expr.type === "variable") {
|
|
21
25
|
return variables[expr.variableId] ?? null;
|
|
22
26
|
}
|
|
23
|
-
const recurse = (child) =>
|
|
27
|
+
const recurse = (child) => evaluateSubtree(child.id, getExpression, getChildren, variables);
|
|
24
28
|
const children = getChildren(expr.id);
|
|
25
29
|
if (expr.type === "formula") {
|
|
26
30
|
return children.length > 0 ? recurse(children[0]) : null;
|
|
27
31
|
}
|
|
28
32
|
switch (expr.operator) {
|
|
29
33
|
case "not":
|
|
30
|
-
return children.length > 0 ?
|
|
34
|
+
return children.length > 0 ? belnapNot(recurse(children[0])) : null;
|
|
31
35
|
case "and":
|
|
32
|
-
return children.reduce((acc, child) =>
|
|
36
|
+
return children.reduce((acc, child) => belnapAnd(acc, recurse(child)), true);
|
|
33
37
|
case "or":
|
|
34
|
-
return children.reduce((acc, child) =>
|
|
38
|
+
return children.reduce((acc, child) => belnapOr(acc, recurse(child)), false);
|
|
35
39
|
case "implies":
|
|
36
40
|
return children.length >= 2
|
|
37
|
-
?
|
|
41
|
+
? belnapImplies(recurse(children[0]), recurse(children[1]))
|
|
38
42
|
: null;
|
|
39
43
|
case "iff":
|
|
40
44
|
return children.length >= 2
|
|
41
|
-
?
|
|
45
|
+
? belnapIff(recurse(children[0]), recurse(children[1]))
|
|
42
46
|
: null;
|
|
43
47
|
default:
|
|
44
48
|
return null;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
|
-
* Run
|
|
49
|
-
*
|
|
50
|
-
*
|
|
52
|
+
* Run constraint propagation to a fixed point over the operators the reader
|
|
53
|
+
* accepted, filling in variable values the granted steps force.
|
|
54
|
+
*
|
|
55
|
+
* Only acceptances propagate. A rejection is not a truth value: it strikes the
|
|
56
|
+
* premise it lives in, and the caller excludes that premise here via
|
|
57
|
+
* `options.excludedPremiseIds` — so nothing inside a struck premise
|
|
58
|
+
* contributes, and no value is ever forced `false` by a refusal.
|
|
59
|
+
*
|
|
60
|
+
* Each step **merges** what it forces into the variable's current value rather
|
|
61
|
+
* than overwriting it or declining to write, so two steps that force opposite
|
|
62
|
+
* values leave the variable `CONTESTED` instead of letting whichever step ran
|
|
63
|
+
* first decide. That merge is the join of the knowledge order, every rule's
|
|
64
|
+
* trigger is monotone in that same order, and the state space is finite — so
|
|
65
|
+
* the sweep converges to the least fixed point above the reader's assignment
|
|
66
|
+
* and reaches it whatever order premises, expressions and rules are visited
|
|
67
|
+
* in.
|
|
68
|
+
*
|
|
69
|
+
* Each rule moves **one truth component in one direction**, and that is not
|
|
70
|
+
* decoration: an accepted `A → B` fires forward on `A` being told true and
|
|
71
|
+
* merges told-true into `B`, and backward on `B` being told false merging
|
|
72
|
+
* told-false into `A`. Transferring both components at once would read the
|
|
73
|
+
* conditional as a biconditional and derive `B` false from `A` false. The
|
|
74
|
+
* one-directional pairing is what a material implication licenses; only `iff`
|
|
75
|
+
* carries both components both ways.
|
|
76
|
+
*
|
|
77
|
+
* Because only the told-true component travels forward, a contested variable
|
|
78
|
+
* can produce an uncontested `true` downstream and leave every aggregate fact
|
|
79
|
+
* reading clean. `evaluateArgument` reports `contestedVariableIds` so a
|
|
80
|
+
* conflict is never inferred from the aggregates. Attribution's counterfactual depends on that: withholding an assertion
|
|
81
|
+
* and re-closing must give one answer, and must not let mutually supporting
|
|
82
|
+
* premises certify each other.
|
|
83
|
+
*
|
|
84
|
+
* A reader's own assertion takes part in the merge like any other source. If
|
|
85
|
+
* the reader asserts a value that a granted step contradicts, the result is
|
|
86
|
+
* `CONTESTED` — the conflict is reported, not silently resolved in either
|
|
87
|
+
* direction.
|
|
51
88
|
*
|
|
52
89
|
* Axiomatic-bound variables are forced to `true` by `ArgumentEngine`'s
|
|
53
|
-
* pre-pass before this function runs
|
|
54
|
-
* `userAssigned` set and are immune to overwrite. Rejecting an operator
|
|
55
|
-
* whose only unknown child is axiom-bound is therefore a propagation no-op
|
|
56
|
-
* on that branch — the axiom stays true and the rejection's downstream
|
|
57
|
-
* propagation halts gracefully.
|
|
90
|
+
* pre-pass before this function runs, and are merged on the same footing.
|
|
58
91
|
*/
|
|
59
|
-
export function propagateOperatorConstraints(ctx, assignment) {
|
|
92
|
+
export function propagateOperatorConstraints(ctx, assignment, options) {
|
|
93
|
+
return closeUnderAcceptedOperators(ctx, assignment, options).variables;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `propagateOperatorConstraints` plus the provenance of every value it saw or
|
|
97
|
+
* produced. The two share one closure, so a tag is recorded where the value is
|
|
98
|
+
* actually set rather than reconstructed afterwards.
|
|
99
|
+
*/
|
|
100
|
+
export function closeUnderAcceptedOperators(ctx, assignment, options) {
|
|
60
101
|
const vars = { ...assignment.variables };
|
|
102
|
+
for (const variableId of options?.withheldVariableIds ?? []) {
|
|
103
|
+
delete vars[variableId];
|
|
104
|
+
}
|
|
61
105
|
const opAssignments = assignment.operatorAssignments;
|
|
106
|
+
const excludedPremiseIds = options?.excludedPremiseIds;
|
|
62
107
|
// Collect all expressions across all premises, indexed by id
|
|
63
108
|
const exprById = new Map();
|
|
109
|
+
// Expression id -> the premise it belongs to
|
|
110
|
+
const premiseIdOf = new Map();
|
|
64
111
|
// Children lookup: parentId -> sorted children
|
|
65
112
|
const childrenOf = new Map();
|
|
66
113
|
for (const pm of ctx.listPremises()) {
|
|
114
|
+
if (excludedPremiseIds?.has(pm.getId()))
|
|
115
|
+
continue;
|
|
67
116
|
for (const expr of pm.getExpressions()) {
|
|
68
117
|
exprById.set(expr.id, expr);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (const expr of pm.getExpressions()) {
|
|
118
|
+
premiseIdOf.set(expr.id, pm.getId());
|
|
119
|
+
// Build children map using getChildExpressions for each operator/formula
|
|
72
120
|
if (expr.type === "operator" || expr.type === "formula") {
|
|
73
121
|
childrenOf.set(expr.id, pm.getChildExpressions(expr.id));
|
|
74
122
|
}
|
|
75
123
|
}
|
|
76
124
|
}
|
|
77
125
|
/**
|
|
78
|
-
* Resolve the current
|
|
79
|
-
*
|
|
80
|
-
*
|
|
126
|
+
* Resolve the current four-valued value of an expression subtree given the
|
|
127
|
+
* current variable assignments. Does not force-accept nested operators —
|
|
128
|
+
* evaluates them normally.
|
|
81
129
|
*/
|
|
82
130
|
const resolveValue = (exprId) => {
|
|
83
131
|
const expr = exprById.get(exprId);
|
|
84
132
|
if (!expr)
|
|
85
133
|
return null;
|
|
86
134
|
if (expr.type === "variable") {
|
|
87
|
-
return
|
|
88
|
-
.variableId] ?? null);
|
|
135
|
+
return vars[expr.variableId] ?? null;
|
|
89
136
|
}
|
|
90
137
|
if (expr.type === "formula") {
|
|
91
138
|
const children = childrenOf.get(expr.id) ?? [];
|
|
@@ -94,18 +141,27 @@ export function propagateOperatorConstraints(ctx, assignment) {
|
|
|
94
141
|
// operator
|
|
95
142
|
const op = expr.operator;
|
|
96
143
|
const children = childrenOf.get(expr.id) ?? [];
|
|
144
|
+
// Arity is guarded rather than assumed: this function is reachable
|
|
145
|
+
// from the exported closure, which a caller may hand a tree that
|
|
146
|
+
// never passed `validateEvaluability()`.
|
|
97
147
|
switch (op) {
|
|
98
148
|
case "not":
|
|
99
|
-
return
|
|
149
|
+
return children.length > 0
|
|
150
|
+
? belnapNot(resolveValue(children[0].id))
|
|
151
|
+
: null;
|
|
100
152
|
case "and":
|
|
101
|
-
return children.reduce((acc, child) =>
|
|
153
|
+
return children.reduce((acc, child) => belnapAnd(acc, resolveValue(child.id)), true);
|
|
102
154
|
case "or":
|
|
103
|
-
return children.reduce((acc, child) =>
|
|
155
|
+
return children.reduce((acc, child) => belnapOr(acc, resolveValue(child.id)), false);
|
|
104
156
|
case "implies": {
|
|
105
|
-
return
|
|
157
|
+
return children.length >= 2
|
|
158
|
+
? belnapImplies(resolveValue(children[0].id), resolveValue(children[1].id))
|
|
159
|
+
: null;
|
|
106
160
|
}
|
|
107
161
|
case "iff": {
|
|
108
|
-
return
|
|
162
|
+
return children.length >= 2
|
|
163
|
+
? belnapIff(resolveValue(children[0].id), resolveValue(children[1].id))
|
|
164
|
+
: null;
|
|
109
165
|
}
|
|
110
166
|
}
|
|
111
167
|
};
|
|
@@ -125,195 +181,185 @@ export function propagateOperatorConstraints(ctx, assignment) {
|
|
|
125
181
|
}
|
|
126
182
|
return null;
|
|
127
183
|
};
|
|
128
|
-
|
|
129
|
-
|
|
184
|
+
/** Variable IDs in an expression subtree that currently hold a value. */
|
|
185
|
+
const collectValuedVariableIds = (exprId) => {
|
|
186
|
+
const expr = exprById.get(exprId);
|
|
187
|
+
if (!expr)
|
|
188
|
+
return [];
|
|
189
|
+
if (expr.type === "variable") {
|
|
190
|
+
return (vars[expr.variableId] ?? null) === null
|
|
191
|
+
? []
|
|
192
|
+
: [expr.variableId];
|
|
193
|
+
}
|
|
194
|
+
return (childrenOf.get(expr.id) ?? []).flatMap((child) => collectValuedVariableIds(child.id));
|
|
195
|
+
};
|
|
196
|
+
// Variable IDs the reader supplied a value for. Propagation merges into
|
|
197
|
+
// them like any other source; the set only tags provenance.
|
|
130
198
|
const userAssigned = new Set();
|
|
131
199
|
for (const [varId, val] of Object.entries(vars)) {
|
|
132
200
|
if (val !== null && val !== undefined)
|
|
133
201
|
userAssigned.add(varId);
|
|
134
202
|
}
|
|
135
203
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
204
|
+
* Every granted step that contributed a component to a variable, keyed by
|
|
205
|
+
* the step's expression and the value it forced. A step is recorded each
|
|
206
|
+
* time its rule fires, whether or not the merge changed anything, and the
|
|
207
|
+
* record is overwritten — so at the fixed point every entry carries
|
|
208
|
+
* `fromVariableIds` read off the converged state rather than off whatever
|
|
209
|
+
* was known the first time the rule happened to run.
|
|
140
210
|
*/
|
|
141
|
-
const
|
|
211
|
+
const contributions = new Map();
|
|
212
|
+
/**
|
|
213
|
+
* Merge a value into a child expression's leaf variable and record the
|
|
214
|
+
* step that forced it. Returns true iff the variable gained a component
|
|
215
|
+
* it did not already have.
|
|
216
|
+
*/
|
|
217
|
+
const mergeIntoChild = (child, value, step) => {
|
|
142
218
|
const varId = resolveLeafVariableId(child);
|
|
143
|
-
if (varId == null
|
|
219
|
+
if (varId == null)
|
|
144
220
|
return false;
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
221
|
+
let byStep = contributions.get(varId);
|
|
222
|
+
if (!byStep) {
|
|
223
|
+
byStep = new Map();
|
|
224
|
+
contributions.set(varId, byStep);
|
|
149
225
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
226
|
+
byStep.set(`${step.expressionId}|${String(value)}`, { step, value });
|
|
227
|
+
const current = vars[varId] ?? null;
|
|
228
|
+
const merged = joinKnowledge(current, value);
|
|
229
|
+
if (merged === current)
|
|
230
|
+
return false;
|
|
231
|
+
vars[varId] = merged;
|
|
232
|
+
return true;
|
|
156
233
|
};
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
case "and": {
|
|
185
|
-
// A ∧ B accepted => all children must be true
|
|
186
|
-
for (const child of children) {
|
|
187
|
-
if (trySetChild(child, true))
|
|
188
|
-
changed = true;
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case "or": {
|
|
193
|
-
// A ∨ B accepted: if all-but-one are false, remaining must be true
|
|
194
|
-
const unknownChildren = [];
|
|
195
|
-
let allOthersAreFalse = true;
|
|
196
|
-
for (const child of children) {
|
|
197
|
-
const childValue = resolveValue(child.id);
|
|
198
|
-
if (childValue === null) {
|
|
199
|
-
unknownChildren.push(child);
|
|
200
|
-
}
|
|
201
|
-
else if (childValue !== false) {
|
|
202
|
-
allOthersAreFalse = false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (unknownChildren.length === 1 &&
|
|
206
|
-
allOthersAreFalse) {
|
|
207
|
-
if (trySetChild(unknownChildren[0], true))
|
|
208
|
-
changed = true;
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
case "implies": {
|
|
213
|
-
// A → B accepted: if A=true => B=true; if B=false => A=false
|
|
214
|
-
if (children.length >= 2) {
|
|
215
|
-
const leftValue = resolveValue(children[0].id);
|
|
216
|
-
const rightValue = resolveValue(children[1].id);
|
|
217
|
-
if (leftValue === true) {
|
|
218
|
-
if (trySetChild(children[1], true))
|
|
219
|
-
changed = true;
|
|
220
|
-
}
|
|
221
|
-
if (rightValue === false) {
|
|
222
|
-
if (trySetChild(children[0], false))
|
|
223
|
-
changed = true;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
case "iff": {
|
|
229
|
-
// A ↔ B accepted: if A known => B matches; if B known => A matches
|
|
230
|
-
if (children.length >= 2) {
|
|
231
|
-
const leftValue = resolveValue(children[0].id);
|
|
232
|
-
const rightValue = resolveValue(children[1].id);
|
|
233
|
-
if (leftValue !== null) {
|
|
234
|
-
if (trySetChild(children[1], leftValue))
|
|
235
|
-
changed = true;
|
|
236
|
-
}
|
|
237
|
-
if (rightValue !== null) {
|
|
238
|
-
if (trySetChild(children[0], rightValue))
|
|
239
|
-
changed = true;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
234
|
+
// One pass over accepted operators; a rejection propagates nothing. Every
|
|
235
|
+
// trigger below reads a truth *component* rather than an exact value, so
|
|
236
|
+
// it can only start holding as the closure learns more, never stop.
|
|
237
|
+
let changed = true;
|
|
238
|
+
while (changed) {
|
|
239
|
+
changed = false;
|
|
240
|
+
for (const [exprId, expr] of exprById) {
|
|
241
|
+
if (expr.type !== "operator")
|
|
242
|
+
continue;
|
|
243
|
+
if (opAssignments[exprId] !== "accepted")
|
|
244
|
+
continue;
|
|
245
|
+
const op = expr.operator;
|
|
246
|
+
const children = childrenOf.get(exprId) ?? [];
|
|
247
|
+
const stepFrom = (consumedExpressionIds) => ({
|
|
248
|
+
expressionId: exprId,
|
|
249
|
+
premiseId: premiseIdOf.get(exprId),
|
|
250
|
+
fromVariableIds: [
|
|
251
|
+
...new Set(consumedExpressionIds.flatMap(collectValuedVariableIds)),
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
switch (op) {
|
|
255
|
+
case "not": {
|
|
256
|
+
// ¬A accepted (= true) => child must be false
|
|
257
|
+
if (children.length > 0) {
|
|
258
|
+
if (mergeIntoChild(children[0], false, stepFrom([])))
|
|
259
|
+
changed = true;
|
|
244
260
|
}
|
|
261
|
+
break;
|
|
245
262
|
}
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
case "and": {
|
|
264
|
+
// A ∧ B accepted => all children must be true
|
|
265
|
+
for (const child of children) {
|
|
266
|
+
if (mergeIntoChild(child, true, stepFrom([])))
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "or": {
|
|
272
|
+
// A ∨ B accepted: a child whose every sibling is known
|
|
273
|
+
// false must itself be true.
|
|
274
|
+
const isFalse = children.map((child) => hasFalseComponent(resolveValue(child.id)));
|
|
275
|
+
for (const [index, child] of children.entries()) {
|
|
276
|
+
const siblingsAllFalse = isFalse.every((value, other) => other === index || value);
|
|
277
|
+
if (!siblingsAllFalse)
|
|
278
|
+
continue;
|
|
279
|
+
const consumed = children
|
|
280
|
+
.filter((_, other) => other !== index)
|
|
281
|
+
.map((sibling) => sibling.id);
|
|
282
|
+
if (mergeIntoChild(child, true, stepFrom(consumed)))
|
|
283
|
+
changed = true;
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "implies": {
|
|
288
|
+
// A → B accepted: A true => B true; B false => A false
|
|
289
|
+
if (children.length >= 2) {
|
|
290
|
+
const leftValue = resolveValue(children[0].id);
|
|
291
|
+
const rightValue = resolveValue(children[1].id);
|
|
292
|
+
if (hasTrueComponent(leftValue)) {
|
|
293
|
+
if (mergeIntoChild(children[1], true, stepFrom([children[0].id])))
|
|
294
|
+
changed = true;
|
|
256
295
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let allOthersAreTrue = true;
|
|
261
|
-
for (const child of children) {
|
|
262
|
-
const childValue = resolveValue(child.id);
|
|
263
|
-
if (childValue === null) {
|
|
264
|
-
unknownChildren.push(child);
|
|
265
|
-
}
|
|
266
|
-
else if (childValue !== true) {
|
|
267
|
-
allOthersAreTrue = false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (unknownChildren.length === 1 &&
|
|
271
|
-
allOthersAreTrue) {
|
|
272
|
-
if (trySetChild(unknownChildren[0], false))
|
|
273
|
-
changed = true;
|
|
274
|
-
}
|
|
275
|
-
break;
|
|
296
|
+
if (hasFalseComponent(rightValue)) {
|
|
297
|
+
if (mergeIntoChild(children[0], false, stepFrom([children[1].id])))
|
|
298
|
+
changed = true;
|
|
276
299
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "iff": {
|
|
304
|
+
// A ↔ B accepted: each side carries its components across
|
|
305
|
+
if (children.length >= 2) {
|
|
306
|
+
const sides = [
|
|
307
|
+
[children[0], children[1]],
|
|
308
|
+
[children[1], children[0]],
|
|
309
|
+
];
|
|
310
|
+
for (const [source, target] of sides) {
|
|
311
|
+
const sourceValue = resolveValue(source.id);
|
|
312
|
+
const step = stepFrom([source.id]);
|
|
313
|
+
if (hasTrueComponent(sourceValue)) {
|
|
314
|
+
if (mergeIntoChild(target, true, step))
|
|
281
315
|
changed = true;
|
|
282
316
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
case "implies": {
|
|
286
|
-
// A → B rejected (= false) => A must be true, B must be false
|
|
287
|
-
if (children.length >= 2) {
|
|
288
|
-
if (trySetChild(children[0], true))
|
|
317
|
+
if (hasFalseComponent(sourceValue)) {
|
|
318
|
+
if (mergeIntoChild(target, false, step))
|
|
289
319
|
changed = true;
|
|
290
|
-
if (trySetChild(children[1], false))
|
|
291
|
-
changed = true;
|
|
292
|
-
}
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
case "iff": {
|
|
296
|
-
// A ↔ B rejected (= false): if A known => B is opposite; if B known => A is opposite
|
|
297
|
-
if (children.length >= 2) {
|
|
298
|
-
const leftValue = resolveValue(children[0].id);
|
|
299
|
-
const rightValue = resolveValue(children[1].id);
|
|
300
|
-
if (leftValue !== null) {
|
|
301
|
-
if (trySetChild(children[1], !leftValue))
|
|
302
|
-
changed = true;
|
|
303
|
-
}
|
|
304
|
-
if (rightValue !== null) {
|
|
305
|
-
if (trySetChild(children[0], !rightValue))
|
|
306
|
-
changed = true;
|
|
307
|
-
}
|
|
308
320
|
}
|
|
309
|
-
break;
|
|
310
321
|
}
|
|
311
322
|
}
|
|
323
|
+
break;
|
|
312
324
|
}
|
|
313
325
|
}
|
|
314
326
|
}
|
|
315
327
|
}
|
|
316
|
-
|
|
328
|
+
/** Stable step order, so provenance never depends on visitation order. */
|
|
329
|
+
const sortSteps = (entries) => [...entries]
|
|
330
|
+
.sort((a, b) => a.step.premiseId.localeCompare(b.step.premiseId) ||
|
|
331
|
+
a.step.expressionId.localeCompare(b.step.expressionId) ||
|
|
332
|
+
String(a.value).localeCompare(String(b.value)))
|
|
333
|
+
.map((entry) => entry.step);
|
|
334
|
+
const provenance = {};
|
|
335
|
+
for (const varId of new Set([
|
|
336
|
+
...Object.keys(vars),
|
|
337
|
+
...contributions.keys(),
|
|
338
|
+
])) {
|
|
339
|
+
const value = vars[varId] ?? null;
|
|
340
|
+
const steps = sortSteps([...(contributions.get(varId)?.values() ?? [])]);
|
|
341
|
+
if (value === CONTESTED) {
|
|
342
|
+
provenance[varId] = {
|
|
343
|
+
value,
|
|
344
|
+
origin: "contested",
|
|
345
|
+
contestedBy: steps,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
else if (userAssigned.has(varId)) {
|
|
349
|
+
provenance[varId] = { value, origin: "asserted" };
|
|
350
|
+
}
|
|
351
|
+
else if (value !== null && steps.length > 0) {
|
|
352
|
+
provenance[varId] = {
|
|
353
|
+
value,
|
|
354
|
+
origin: "derived",
|
|
355
|
+
derivedBy: steps[0],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
provenance[varId] = { value, origin: "unassigned" };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { variables: vars, provenance };
|
|
317
363
|
}
|
|
318
364
|
/**
|
|
319
365
|
* Evaluates an argument under a three-valued expression assignment.
|
|
@@ -370,42 +416,49 @@ export function evaluateArgument(ctx, assignment, options) {
|
|
|
370
416
|
return true;
|
|
371
417
|
return false;
|
|
372
418
|
});
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
419
|
+
// A rejection strikes the premise it lives in: that premise stops
|
|
420
|
+
// constraining the evaluation and asserts nothing. The conclusion premise
|
|
421
|
+
// and derivation premises are exempt — a rejection recorded against
|
|
422
|
+
// either is ignored, and observably so, because the struck set is
|
|
423
|
+
// reported.
|
|
424
|
+
const struckPremiseIds = allRelevantPremises
|
|
425
|
+
.filter((pm) => pm.getId() !== ctx.conclusionPremiseId &&
|
|
426
|
+
pm.getPremiseType?.() !== "derivation" &&
|
|
427
|
+
pm
|
|
428
|
+
.getExpressions()
|
|
429
|
+
.some((expr) => assignment.operatorAssignments[expr.id] ===
|
|
430
|
+
"rejected"))
|
|
431
|
+
.map((pm) => pm.getId());
|
|
432
|
+
const struckIds = new Set(struckPremiseIds);
|
|
379
433
|
try {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return propagatedAssignment.variables[variableId] ?? null;
|
|
394
|
-
}
|
|
395
|
-
// Internal premise-bound: lazy resolution
|
|
396
|
-
const boundPremiseId = variable.boundPremiseId;
|
|
397
|
-
const boundPremise = ctx.getPremise(boundPremiseId);
|
|
398
|
-
if (!boundPremise) {
|
|
399
|
-
resolverCache.set(variableId, null);
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
const premiseResult = boundPremise.evaluate(propagatedAssignment, {
|
|
403
|
-
resolver,
|
|
434
|
+
const premiseSetSatisfiable = options?.premiseSetSatisfiable !== undefined
|
|
435
|
+
? options.premiseSetSatisfiable
|
|
436
|
+
: isPremiseSetSatisfiable(ctx, {
|
|
437
|
+
premises: [
|
|
438
|
+
...supportingPremises,
|
|
439
|
+
...constraintPremises,
|
|
440
|
+
].filter((pm) => !struckIds.has(pm.getId())),
|
|
441
|
+
freeVariableIds: referencedVariableIds,
|
|
442
|
+
// Deliberately not `forcedTrueVariableIds`. That set
|
|
443
|
+
// also decides what counts as a reader's assertion, and
|
|
444
|
+
// a citation belongs in this question but not that one.
|
|
445
|
+
forcedTrueVariableIds: options?.satisfiabilityForcedTrueVariableIds ??
|
|
446
|
+
options?.forcedTrueVariableIds,
|
|
404
447
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
448
|
+
// Contradicting premises license nothing: exclude every premise from
|
|
449
|
+
// the closure so the reader is shown only what they asserted.
|
|
450
|
+
const derivationSuppressed = premiseSetSatisfiable === false;
|
|
451
|
+
const closureExclusions = derivationSuppressed
|
|
452
|
+
? new Set(ctx.listPremises().map((pm) => pm.getId()))
|
|
453
|
+
: struckIds;
|
|
454
|
+
const propagation = closeUnderAcceptedOperators(ctx, assignment, {
|
|
455
|
+
excludedPremiseIds: closureExclusions,
|
|
456
|
+
});
|
|
457
|
+
const propagatedAssignment = {
|
|
458
|
+
variables: propagation.variables,
|
|
459
|
+
operatorAssignments: assignment.operatorAssignments,
|
|
408
460
|
};
|
|
461
|
+
const resolver = createPremiseBoundResolver(ctx, propagatedAssignment);
|
|
409
462
|
const evalOpts = {
|
|
410
463
|
strictUnknownKeys: options?.strictUnknownAssignmentKeys ?? false,
|
|
411
464
|
resolver,
|
|
@@ -413,12 +466,100 @@ export function evaluateArgument(ctx, assignment, options) {
|
|
|
413
466
|
const conclusionEvaluation = conclusion.evaluate(propagatedAssignment, evalOpts);
|
|
414
467
|
const supportingEvaluations = supportingPremises.map((pm) => pm.evaluate(propagatedAssignment, evalOpts));
|
|
415
468
|
const constraintEvaluations = constraintPremises.map((pm) => pm.evaluate(propagatedAssignment, evalOpts));
|
|
416
|
-
const
|
|
417
|
-
const
|
|
469
|
+
const surviving = (results) => results.filter((result) => !struckIds.has(result.premiseId));
|
|
470
|
+
const isAdmissibleAssignment = surviving(constraintEvaluations).reduce((acc, result) => belnapAnd(acc, result.rootValue ?? null), true);
|
|
471
|
+
const survivingSupport = surviving(supportingEvaluations);
|
|
472
|
+
const survivingSupportingPremisesTrue = survivingSupport.reduce((acc, result) => belnapAnd(acc, result.rootValue ?? null), true);
|
|
418
473
|
const conclusionTrue = conclusionEvaluation.rootValue ?? null;
|
|
419
|
-
|
|
474
|
+
// `survivingSupportingPremisesTrue` folds an empty list to `true`, so
|
|
475
|
+
// when the reader struck every supporting premise there is no case left
|
|
476
|
+
// to weigh — say so rather than reporting that the premises held and
|
|
477
|
+
// the conclusion failed. An argument authored with no supporting
|
|
478
|
+
// premises is the entailment-from-nothing case and keeps its answer.
|
|
479
|
+
const allSupportStruck = supportingEvaluations.length > 0 && survivingSupport.length === 0;
|
|
480
|
+
const premisesHoldConclusionFalse = allSupportStruck
|
|
481
|
+
? null
|
|
482
|
+
: belnapAnd(isAdmissibleAssignment, belnapAnd(survivingSupportingPremisesTrue, belnapNot(conclusionTrue)));
|
|
483
|
+
// Reported unconditionally, not behind `includeDiagnostics`: a
|
|
484
|
+
// contested value can leave every aggregate above reading clean, so
|
|
485
|
+
// this is the only fact that always records one.
|
|
486
|
+
const contestedVariableIds = Object.entries(propagation.provenance)
|
|
487
|
+
.filter(([, entry]) => entry.value === CONTESTED)
|
|
488
|
+
.map(([variableId]) => variableId)
|
|
489
|
+
.sort();
|
|
420
490
|
const includeExpressionValues = options?.includeExpressionValues ?? true;
|
|
421
491
|
const includeDiagnostics = options?.includeDiagnostics ?? true;
|
|
492
|
+
// Attribution: withhold an assertion, recompute closure from what is
|
|
493
|
+
// left, and ask again. Never delete a tag from an already-derived
|
|
494
|
+
// value — the intervention has to be re-derived, not un-derived.
|
|
495
|
+
const forcedTrueVariableIds = options?.forcedTrueVariableIds;
|
|
496
|
+
const isReaderAsserted = (variableId) => forcedTrueVariableIds?.has(variableId) !== true &&
|
|
497
|
+
(assignment.variables[variableId] ?? null) !== null;
|
|
498
|
+
const canDerive = !derivationSuppressed &&
|
|
499
|
+
Object.values(assignment.operatorAssignments).includes("accepted");
|
|
500
|
+
const withhold = (withheldVariableIds) => {
|
|
501
|
+
if (!canDerive) {
|
|
502
|
+
// Nothing can be derived, so closure is the seed minus what
|
|
503
|
+
// was withheld.
|
|
504
|
+
const reduced = { ...assignment.variables };
|
|
505
|
+
for (const variableId of withheldVariableIds)
|
|
506
|
+
delete reduced[variableId];
|
|
507
|
+
return reduced;
|
|
508
|
+
}
|
|
509
|
+
return closeUnderAcceptedOperators(ctx, assignment, {
|
|
510
|
+
excludedPremiseIds: closureExclusions,
|
|
511
|
+
withheldVariableIds,
|
|
512
|
+
}).variables;
|
|
513
|
+
};
|
|
514
|
+
const conclusionClaimVariableIds = [
|
|
515
|
+
...new Set(conclusion
|
|
516
|
+
.getExpressions()
|
|
517
|
+
.filter((expr) => expr.type === "variable")
|
|
518
|
+
.map((expr) => expr.variableId)),
|
|
519
|
+
].filter((vid) => {
|
|
520
|
+
if (forcedTrueVariableIds?.has(vid) === true)
|
|
521
|
+
return false;
|
|
522
|
+
const variable = ctx.getVariable(vid);
|
|
523
|
+
return variable != null && isClaimBound(variable);
|
|
524
|
+
});
|
|
525
|
+
let reachedWithoutAssertion = conclusionTrue === true;
|
|
526
|
+
if (conclusionClaimVariableIds.length > 0) {
|
|
527
|
+
const counterfactual = {
|
|
528
|
+
variables: withhold(new Set(conclusionClaimVariableIds)),
|
|
529
|
+
operatorAssignments: assignment.operatorAssignments,
|
|
530
|
+
};
|
|
531
|
+
const rootValue = conclusion.evaluate(counterfactual, {
|
|
532
|
+
strictUnknownKeys: false,
|
|
533
|
+
resolver: createPremiseBoundResolver(ctx, counterfactual),
|
|
534
|
+
}).rootValue;
|
|
535
|
+
reachedWithoutAssertion = (rootValue ?? null) === true;
|
|
536
|
+
}
|
|
537
|
+
const conclusionAttribution = {
|
|
538
|
+
assertedByReader: conclusionClaimVariableIds.some(isReaderAsserted),
|
|
539
|
+
reachedWithoutAssertion,
|
|
540
|
+
};
|
|
541
|
+
// One closure per reader-asserted claim, and only when something could
|
|
542
|
+
// have been derived at all.
|
|
543
|
+
const claimAttribution = includeDiagnostics && canDerive
|
|
544
|
+
? Object.fromEntries(referencedVariableIds
|
|
545
|
+
.filter((vid) => {
|
|
546
|
+
const variable = ctx.getVariable(vid);
|
|
547
|
+
return (variable != null &&
|
|
548
|
+
isClaimBound(variable) &&
|
|
549
|
+
isReaderAsserted(vid));
|
|
550
|
+
})
|
|
551
|
+
.map((vid) => {
|
|
552
|
+
const asserted = assignment.variables[vid];
|
|
553
|
+
const reclosed = withhold(new Set([vid]));
|
|
554
|
+
return [
|
|
555
|
+
vid,
|
|
556
|
+
{
|
|
557
|
+
assertedByReader: true,
|
|
558
|
+
reachedWithoutAssertion: (reclosed[vid] ?? null) === asserted,
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
}))
|
|
562
|
+
: undefined;
|
|
422
563
|
const strip = (result) => ({
|
|
423
564
|
...result,
|
|
424
565
|
expressionValues: includeExpressionValues
|
|
@@ -434,6 +575,15 @@ export function evaluateArgument(ctx, assignment, options) {
|
|
|
434
575
|
propagatedAssignment.variables[vid] ?? null,
|
|
435
576
|
]))
|
|
436
577
|
: undefined;
|
|
578
|
+
const variableProvenance = includeDiagnostics
|
|
579
|
+
? Object.fromEntries(referencedVariableIds.map((vid) => [
|
|
580
|
+
vid,
|
|
581
|
+
propagation.provenance[vid] ?? {
|
|
582
|
+
value: null,
|
|
583
|
+
origin: "unassigned",
|
|
584
|
+
},
|
|
585
|
+
]))
|
|
586
|
+
: undefined;
|
|
437
587
|
return {
|
|
438
588
|
ok: true,
|
|
439
589
|
assignment: {
|
|
@@ -446,12 +596,18 @@ export function evaluateArgument(ctx, assignment, options) {
|
|
|
446
596
|
conclusion: strip(conclusionEvaluation),
|
|
447
597
|
supportingPremises: supportingEvaluations.map(strip),
|
|
448
598
|
constraintPremises: constraintEvaluations.map(strip),
|
|
599
|
+
struckPremiseIds,
|
|
600
|
+
survivingSupportingPremiseCount: survivingSupport.length,
|
|
449
601
|
isAdmissibleAssignment,
|
|
450
|
-
|
|
602
|
+
survivingSupportingPremisesTrue,
|
|
451
603
|
conclusionTrue,
|
|
452
|
-
|
|
453
|
-
|
|
604
|
+
premisesHoldConclusionFalse,
|
|
605
|
+
contestedVariableIds,
|
|
606
|
+
conclusionAttribution,
|
|
607
|
+
claimAttribution,
|
|
608
|
+
premiseSetSatisfiable,
|
|
454
609
|
propagatedVariableValues,
|
|
610
|
+
variableProvenance,
|
|
455
611
|
};
|
|
456
612
|
}
|
|
457
613
|
catch (error) {
|
|
@@ -542,6 +698,15 @@ export function checkArgumentValidity(ctx, options) {
|
|
|
542
698
|
]),
|
|
543
699
|
};
|
|
544
700
|
}
|
|
701
|
+
// The generated assignments carry no operator decisions, so nothing is
|
|
702
|
+
// ever struck and the premise set is the same on every row. Computing it
|
|
703
|
+
// once here and threading it through keeps the search 2^n rather than
|
|
704
|
+
// 2^n × 2^n.
|
|
705
|
+
const premiseSetSatisfiable = isPremiseSetSatisfiable(ctx, {
|
|
706
|
+
premises: [...supportingPremises, ...constraintPremises],
|
|
707
|
+
freeVariableIds: checkedVariableIds,
|
|
708
|
+
forcedTrueVariableIds,
|
|
709
|
+
});
|
|
545
710
|
const mode = options?.mode ?? "firstCounterexample";
|
|
546
711
|
const maxAssignmentsChecked = options?.maxAssignmentsChecked;
|
|
547
712
|
const counterexamples = [];
|
|
@@ -571,6 +736,8 @@ export function checkArgumentValidity(ctx, options) {
|
|
|
571
736
|
validateFirst: false,
|
|
572
737
|
includeExpressionValues: options?.includeCounterexampleEvaluations ?? false,
|
|
573
738
|
includeDiagnostics: options?.includeCounterexampleEvaluations ?? false,
|
|
739
|
+
forcedTrueVariableIds,
|
|
740
|
+
premiseSetSatisfiable,
|
|
574
741
|
});
|
|
575
742
|
if (!result.ok) {
|
|
576
743
|
return {
|
|
@@ -582,7 +749,7 @@ export function checkArgumentValidity(ctx, options) {
|
|
|
582
749
|
if (result.isAdmissibleAssignment === true) {
|
|
583
750
|
numAdmissibleAssignments += 1;
|
|
584
751
|
}
|
|
585
|
-
if (result.
|
|
752
|
+
if (result.premisesHoldConclusionFalse === true) {
|
|
586
753
|
counterexamples.push({
|
|
587
754
|
assignment: result.assignment,
|
|
588
755
|
result,
|