@polintpro/proposit-core 0.6.6 → 0.7.5
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 +201 -0
- package/dist/extensions/basics/schemata.d.ts +7 -0
- package/dist/extensions/basics/schemata.d.ts.map +1 -1
- package/dist/lib/consts.d.ts.map +1 -1
- package/dist/lib/consts.js +34 -2
- package/dist/lib/consts.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +38 -2
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +636 -227
- package/dist/lib/core/argument-engine.js.map +1 -1
- package/dist/lib/core/change-collector.d.ts +1 -0
- package/dist/lib/core/change-collector.d.ts.map +1 -1
- package/dist/lib/core/change-collector.js +3 -0
- package/dist/lib/core/change-collector.js.map +1 -1
- package/dist/lib/core/claim-library.d.ts +4 -0
- package/dist/lib/core/claim-library.d.ts.map +1 -1
- package/dist/lib/core/claim-library.js +126 -59
- package/dist/lib/core/claim-library.js.map +1 -1
- package/dist/lib/core/claim-source-library.d.ts +4 -0
- package/dist/lib/core/claim-source-library.d.ts.map +1 -1
- package/dist/lib/core/claim-source-library.js +114 -38
- package/dist/lib/core/claim-source-library.js.map +1 -1
- package/dist/lib/core/diff.d.ts +10 -0
- package/dist/lib/core/diff.d.ts.map +1 -1
- package/dist/lib/core/diff.js +114 -21
- package/dist/lib/core/diff.js.map +1 -1
- package/dist/lib/core/expression-manager.d.ts +11 -0
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +379 -20
- package/dist/lib/core/expression-manager.js.map +1 -1
- package/dist/lib/core/fork.d.ts +31 -0
- package/dist/lib/core/fork.d.ts.map +1 -0
- package/dist/lib/core/fork.js +139 -0
- package/dist/lib/core/fork.js.map +1 -0
- package/dist/lib/core/forks-library.d.ts +58 -0
- package/dist/lib/core/forks-library.d.ts.map +1 -0
- package/dist/lib/core/forks-library.js +142 -0
- package/dist/lib/core/forks-library.js.map +1 -0
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +9 -2
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/index.d.ts +1 -1
- package/dist/lib/core/interfaces/index.d.ts.map +1 -1
- package/dist/lib/core/interfaces/library.interfaces.d.ts +48 -0
- package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +22 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/invariant-violation-error.d.ts +6 -0
- package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
- package/dist/lib/core/invariant-violation-error.js +12 -0
- package/dist/lib/core/invariant-violation-error.js.map +1 -0
- package/dist/lib/core/parser/formula.d.ts.map +1 -1
- package/dist/lib/core/parser/formula.js +2 -2
- package/dist/lib/core/parser/formula.js.map +1 -1
- package/dist/lib/core/premise-engine.d.ts +10 -0
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +699 -536
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/core/source-library.d.ts +4 -0
- package/dist/lib/core/source-library.d.ts.map +1 -1
- package/dist/lib/core/source-library.js +126 -59
- package/dist/lib/core/source-library.js.map +1 -1
- package/dist/lib/core/variable-manager.d.ts +7 -0
- package/dist/lib/core/variable-manager.d.ts.map +1 -1
- package/dist/lib/core/variable-manager.js +65 -1
- package/dist/lib/core/variable-manager.js.map +1 -1
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +8 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemata/argument.d.ts +3 -0
- package/dist/lib/schemata/argument.d.ts.map +1 -1
- package/dist/lib/schemata/argument.js +9 -0
- package/dist/lib/schemata/argument.js.map +1 -1
- package/dist/lib/schemata/fork.d.ts +17 -0
- package/dist/lib/schemata/fork.d.ts.map +1 -0
- package/dist/lib/schemata/fork.js +27 -0
- package/dist/lib/schemata/fork.js.map +1 -0
- package/dist/lib/schemata/index.d.ts +1 -0
- package/dist/lib/schemata/index.d.ts.map +1 -1
- package/dist/lib/schemata/index.js +1 -0
- package/dist/lib/schemata/index.js.map +1 -1
- package/dist/lib/schemata/propositional.d.ts +52 -0
- package/dist/lib/schemata/propositional.d.ts.map +1 -1
- package/dist/lib/schemata/propositional.js +43 -0
- package/dist/lib/schemata/propositional.js.map +1 -1
- package/dist/lib/types/checksum.d.ts +2 -0
- package/dist/lib/types/checksum.d.ts.map +1 -1
- package/dist/lib/types/diff.d.ts +6 -0
- package/dist/lib/types/diff.d.ts.map +1 -1
- package/dist/lib/types/fork.d.ts +25 -0
- package/dist/lib/types/fork.d.ts.map +1 -0
- package/dist/lib/types/fork.js +2 -0
- package/dist/lib/types/fork.js.map +1 -0
- package/dist/lib/types/grammar.d.ts +5 -4
- package/dist/lib/types/grammar.d.ts.map +1 -1
- package/dist/lib/types/grammar.js.map +1 -1
- package/dist/lib/types/validation.d.ts +47 -0
- package/dist/lib/types/validation.d.ts.map +1 -0
- package/dist/lib/types/validation.js +43 -0
- package/dist/lib/types/validation.js.map +1 -0
- package/dist/lib/utils/changeset.d.ts +124 -0
- package/dist/lib/utils/changeset.d.ts.map +1 -0
- package/dist/lib/utils/changeset.js +221 -0
- package/dist/lib/utils/changeset.js.map +1 -0
- package/dist/lib/utils/lookup.d.ts +47 -0
- package/dist/lib/utils/lookup.d.ts.map +1 -0
- package/dist/lib/utils/lookup.js +62 -0
- package/dist/lib/utils/lookup.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { isPremiseBound, } from "../schemata/index.js";
|
|
2
|
+
import { CorePremiseSchema, isExternallyBound, isPremiseBound, } from "../schemata/index.js";
|
|
3
3
|
import { DefaultMap } from "../utils/default-map.js";
|
|
4
4
|
import { midpoint, POSITION_INITIAL, POSITION_MAX } from "../utils/position.js";
|
|
5
5
|
import { sortedCopyById, sortedUnique } from "../utils/collections.js";
|
|
6
6
|
import { kleeneAnd, kleeneIff, kleeneImplies, kleeneNot, kleeneOr, } from "./evaluation/kleene.js";
|
|
7
7
|
import { buildDirectionalVacuity, makeErrorIssue, makeValidationResult, } from "./evaluation/validation.js";
|
|
8
|
+
import { Value } from "typebox/value";
|
|
9
|
+
import { PREMISE_SCHEMA_INVALID, PREMISE_ROOT_EXPRESSION_INVALID, PREMISE_VARIABLE_REF_NOT_FOUND, } from "../types/validation.js";
|
|
8
10
|
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
9
11
|
import { ChangeCollector } from "./change-collector.js";
|
|
10
12
|
import { computeHash, entityChecksum } from "./checksum.js";
|
|
13
|
+
import { InvariantViolationError } from "./invariant-violation-error.js";
|
|
11
14
|
import { ExpressionManager } from "./expression-manager.js";
|
|
12
15
|
import { VariableManager } from "./variable-manager.js";
|
|
13
16
|
export class PremiseEngine {
|
|
@@ -26,6 +29,9 @@ export class PremiseEngine {
|
|
|
26
29
|
onMutate;
|
|
27
30
|
circularityCheck;
|
|
28
31
|
emptyBoundPremiseCheck;
|
|
32
|
+
variableIdsCallback;
|
|
33
|
+
argumentValidateCallback;
|
|
34
|
+
insideValidation = false;
|
|
29
35
|
constructor(premise, deps, config) {
|
|
30
36
|
this.premise = { ...premise };
|
|
31
37
|
this.argument = deps.argument;
|
|
@@ -45,427 +51,406 @@ export class PremiseEngine {
|
|
|
45
51
|
setEmptyBoundPremiseCheck(check) {
|
|
46
52
|
this.emptyBoundPremiseCheck = check;
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
setVariableIdsCallback(callback) {
|
|
55
|
+
this.variableIdsCallback = callback;
|
|
56
|
+
}
|
|
57
|
+
setArgumentValidateCallback(callback) {
|
|
58
|
+
this.argumentValidateCallback = callback;
|
|
59
|
+
}
|
|
60
|
+
premiseSnapshot() {
|
|
61
|
+
const expressionIndexEntries = [];
|
|
62
|
+
if (this.expressionIndex) {
|
|
63
|
+
for (const [exprId, premiseId] of this.expressionIndex) {
|
|
64
|
+
if (premiseId === this.premise.id) {
|
|
65
|
+
expressionIndexEntries.push([exprId, premiseId]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const e of changes.expressions.removed) {
|
|
70
|
-
collector.removedExpression(e);
|
|
71
|
-
}
|
|
69
|
+
return {
|
|
70
|
+
premiseData: { ...this.premise },
|
|
71
|
+
rootExpressionId: this.rootExpressionId,
|
|
72
|
+
expressionSnapshot: this.expressions.snapshot(),
|
|
73
|
+
expressionIndexEntries,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
restoreFromPremiseSnapshot(snap) {
|
|
77
|
+
this.premise = snap.premiseData;
|
|
78
|
+
this.rootExpressionId = snap.rootExpressionId;
|
|
79
|
+
this.expressions = ExpressionManager.fromSnapshot(snap.expressionSnapshot);
|
|
80
|
+
// Restore expression index entries
|
|
81
|
+
if (this.expressionIndex) {
|
|
82
|
+
for (const [exprId, premiseId] of [...this.expressionIndex]) {
|
|
83
|
+
if (premiseId === this.premise.id) {
|
|
84
|
+
this.expressionIndex.delete(exprId);
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const changes = collector.toChangeset();
|
|
77
|
-
this.syncExpressionIndex(changes);
|
|
78
|
-
// Restore and fire once if something was removed
|
|
79
|
-
this.onMutate = savedOnMutate;
|
|
80
|
-
if (removed.length > 0) {
|
|
81
|
-
this.onMutate?.();
|
|
87
|
+
for (const [exprId, premiseId] of snap.expressionIndexEntries) {
|
|
88
|
+
this.expressionIndex.set(exprId, premiseId);
|
|
82
89
|
}
|
|
83
|
-
return {
|
|
84
|
-
result: removed,
|
|
85
|
-
changes,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
this.onMutate = savedOnMutate;
|
|
90
|
-
throw e;
|
|
91
90
|
}
|
|
91
|
+
this.rebuildVariableIndex();
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
!this.variables.hasVariable(expression.variableId)) {
|
|
97
|
-
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
93
|
+
withValidation(fn) {
|
|
94
|
+
if (this.insideValidation) {
|
|
95
|
+
return fn();
|
|
98
96
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const snap = this.premiseSnapshot();
|
|
98
|
+
this.insideValidation = true;
|
|
99
|
+
try {
|
|
100
|
+
const result = fn();
|
|
101
|
+
const validation = this.argumentValidateCallback?.() ?? this.validate();
|
|
102
|
+
if (!validation.ok) {
|
|
103
|
+
this.restoreFromPremiseSnapshot(snap);
|
|
104
|
+
throw new InvariantViolationError(validation.violations);
|
|
102
105
|
}
|
|
106
|
+
return result;
|
|
103
107
|
}
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (!(e instanceof InvariantViolationError)) {
|
|
110
|
+
this.restoreFromPremiseSnapshot(snap);
|
|
107
111
|
}
|
|
112
|
+
throw e;
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
|
|
112
|
-
}
|
|
114
|
+
finally {
|
|
115
|
+
this.insideValidation = false;
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
}
|
|
118
|
+
deleteExpressionsUsingVariable(variableId) {
|
|
119
|
+
return this.withValidation(() => {
|
|
120
|
+
const expressionIds = this.expressionsByVariableId.get(variableId);
|
|
121
|
+
if (expressionIds.size === 0) {
|
|
122
|
+
return { result: [], changes: {} };
|
|
123
|
+
}
|
|
124
|
+
const collector = new ChangeCollector();
|
|
125
|
+
// Suppress onMutate during the loop to avoid redundant notifications
|
|
126
|
+
const savedOnMutate = this.onMutate;
|
|
127
|
+
this.onMutate = undefined;
|
|
128
|
+
try {
|
|
129
|
+
// Copy the set since removeExpression mutates expressionsByVariableId
|
|
130
|
+
const removed = [];
|
|
131
|
+
for (const exprId of [...expressionIds]) {
|
|
132
|
+
// The expression may already have been removed as part of a
|
|
133
|
+
// prior subtree deletion or operator collapse in this loop.
|
|
134
|
+
if (!this.expressions.getExpression(exprId))
|
|
135
|
+
continue;
|
|
136
|
+
const { result, changes } = this.removeExpression(exprId, true);
|
|
137
|
+
if (result)
|
|
138
|
+
removed.push(result);
|
|
139
|
+
if (changes.expressions) {
|
|
140
|
+
for (const e of changes.expressions.removed) {
|
|
141
|
+
collector.removedExpression(e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Expressions in the collector already have checksums attached
|
|
146
|
+
// (from ExpressionManager which stores expressions with checksums).
|
|
147
|
+
const changes = collector.toChangeset();
|
|
148
|
+
this.syncExpressionIndex(changes);
|
|
149
|
+
// Restore and fire once if something was removed
|
|
150
|
+
this.onMutate = savedOnMutate;
|
|
151
|
+
if (removed.length > 0) {
|
|
152
|
+
this.onMutate?.();
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
result: removed,
|
|
156
|
+
changes,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
this.onMutate = savedOnMutate;
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
addExpression(expression) {
|
|
166
|
+
return this.withValidation(() => {
|
|
167
|
+
this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
|
|
168
|
+
if (expression.type === "variable" &&
|
|
169
|
+
!this.variables.hasVariable(expression.variableId)) {
|
|
170
|
+
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
171
|
+
}
|
|
172
|
+
if (expression.type === "variable" && this.circularityCheck) {
|
|
173
|
+
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
174
|
+
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
120
177
|
if (expression.parentId === null) {
|
|
121
|
-
this.rootExpressionId
|
|
178
|
+
if (this.rootExpressionId !== undefined) {
|
|
179
|
+
throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
|
|
180
|
+
}
|
|
122
181
|
}
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
else {
|
|
183
|
+
if (!this.expressions.getExpression(expression.parentId)) {
|
|
184
|
+
throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
|
|
185
|
+
}
|
|
127
186
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
const collector = new ChangeCollector();
|
|
188
|
+
this.expressions.setCollector(collector);
|
|
189
|
+
try {
|
|
190
|
+
// Delegate structural validation (operator type checks, position
|
|
191
|
+
// uniqueness, child limits) to ExpressionManager.
|
|
192
|
+
this.expressions.addExpression(expression);
|
|
193
|
+
if (expression.parentId === null) {
|
|
194
|
+
this.rootExpressionId = expression.id;
|
|
195
|
+
}
|
|
196
|
+
if (expression.type === "variable") {
|
|
197
|
+
this.expressionsByVariableId
|
|
198
|
+
.get(expression.variableId)
|
|
199
|
+
.add(expression.id);
|
|
200
|
+
}
|
|
201
|
+
this.markDirty();
|
|
202
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
203
|
+
this.syncExpressionIndex(changes);
|
|
204
|
+
this.onMutate?.();
|
|
205
|
+
return {
|
|
206
|
+
result: this.expressions.getExpression(expression.id),
|
|
207
|
+
changes,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this.expressions.setCollector(null);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
140
214
|
}
|
|
141
215
|
appendExpression(parentId, expression) {
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
148
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
149
|
-
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
216
|
+
return this.withValidation(() => {
|
|
217
|
+
this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
|
|
218
|
+
if (expression.type === "variable" &&
|
|
219
|
+
!this.variables.hasVariable(expression.variableId)) {
|
|
220
|
+
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
150
221
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
if (!this.expressions.getExpression(parentId)) {
|
|
159
|
-
throw new Error(`Parent expression "${parentId}" does not exist in this premise.`);
|
|
222
|
+
if (expression.type === "variable" && this.circularityCheck) {
|
|
223
|
+
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
224
|
+
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
225
|
+
}
|
|
160
226
|
}
|
|
161
|
-
}
|
|
162
|
-
const collector = new ChangeCollector();
|
|
163
|
-
this.expressions.setCollector(collector);
|
|
164
|
-
try {
|
|
165
|
-
this.expressions.appendExpression(parentId, expression);
|
|
166
227
|
if (parentId === null) {
|
|
167
|
-
this.
|
|
228
|
+
if (this.rootExpressionId !== undefined) {
|
|
229
|
+
throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
|
|
230
|
+
}
|
|
168
231
|
}
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
232
|
+
else {
|
|
233
|
+
if (!this.expressions.getExpression(parentId)) {
|
|
234
|
+
throw new Error(`Parent expression "${parentId}" does not exist in this premise.`);
|
|
235
|
+
}
|
|
173
236
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
237
|
+
const collector = new ChangeCollector();
|
|
238
|
+
this.expressions.setCollector(collector);
|
|
239
|
+
try {
|
|
240
|
+
this.expressions.appendExpression(parentId, expression);
|
|
241
|
+
if (parentId === null) {
|
|
242
|
+
this.syncRootExpressionId();
|
|
243
|
+
}
|
|
244
|
+
if (expression.type === "variable") {
|
|
245
|
+
this.expressionsByVariableId
|
|
246
|
+
.get(expression.variableId)
|
|
247
|
+
.add(expression.id);
|
|
248
|
+
}
|
|
249
|
+
this.markDirty();
|
|
250
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
251
|
+
this.syncExpressionIndex(changes);
|
|
252
|
+
this.onMutate?.();
|
|
253
|
+
return {
|
|
254
|
+
result: this.expressions.getExpression(expression.id),
|
|
255
|
+
changes,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
this.expressions.setCollector(null);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
186
262
|
}
|
|
187
263
|
addExpressionRelative(siblingId, relativePosition, expression) {
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
194
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
195
|
-
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
264
|
+
return this.withValidation(() => {
|
|
265
|
+
this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
|
|
266
|
+
if (expression.type === "variable" &&
|
|
267
|
+
!this.variables.hasVariable(expression.variableId)) {
|
|
268
|
+
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
196
269
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const collector = new ChangeCollector();
|
|
202
|
-
this.expressions.setCollector(collector);
|
|
203
|
-
try {
|
|
204
|
-
this.expressions.addExpressionRelative(siblingId, relativePosition, expression);
|
|
205
|
-
if (expression.type === "variable") {
|
|
206
|
-
this.expressionsByVariableId
|
|
207
|
-
.get(expression.variableId)
|
|
208
|
-
.add(expression.id);
|
|
270
|
+
if (expression.type === "variable" && this.circularityCheck) {
|
|
271
|
+
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
272
|
+
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
273
|
+
}
|
|
209
274
|
}
|
|
210
|
-
this.
|
|
211
|
-
|
|
212
|
-
this.syncExpressionIndex(changes);
|
|
213
|
-
this.onMutate?.();
|
|
214
|
-
return {
|
|
215
|
-
result: this.expressions.getExpression(expression.id),
|
|
216
|
-
changes,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
this.expressions.setCollector(null);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
updateExpression(expressionId, updates) {
|
|
224
|
-
const existing = this.expressions.getExpression(expressionId);
|
|
225
|
-
if (!existing) {
|
|
226
|
-
throw new Error(`Expression "${expressionId}" not found in premise "${this.premise.id}".`);
|
|
227
|
-
}
|
|
228
|
-
if (updates.variableId !== undefined) {
|
|
229
|
-
if (!this.variables.hasVariable(updates.variableId)) {
|
|
230
|
-
throw new Error(`Variable expression "${expressionId}" references non-existent variable "${updates.variableId}".`);
|
|
275
|
+
if (!this.expressions.getExpression(siblingId)) {
|
|
276
|
+
throw new Error(`Expression "${siblingId}" not found in this premise.`);
|
|
231
277
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.expressionsByVariableId
|
|
242
|
-
.get(oldVariableId)
|
|
243
|
-
?.delete(expressionId);
|
|
244
|
-
this.expressionsByVariableId
|
|
245
|
-
.get(updates.variableId)
|
|
246
|
-
.add(expressionId);
|
|
247
|
-
}
|
|
248
|
-
const changeset = this.flushAndBuildChangeset(collector);
|
|
249
|
-
if (changeset.expressions !== undefined) {
|
|
278
|
+
const collector = new ChangeCollector();
|
|
279
|
+
this.expressions.setCollector(collector);
|
|
280
|
+
try {
|
|
281
|
+
this.expressions.addExpressionRelative(siblingId, relativePosition, expression);
|
|
282
|
+
if (expression.type === "variable") {
|
|
283
|
+
this.expressionsByVariableId
|
|
284
|
+
.get(expression.variableId)
|
|
285
|
+
.add(expression.id);
|
|
286
|
+
}
|
|
250
287
|
this.markDirty();
|
|
288
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
289
|
+
this.syncExpressionIndex(changes);
|
|
251
290
|
this.onMutate?.();
|
|
252
|
-
}
|
|
253
|
-
this.syncExpressionIndex(changeset);
|
|
254
|
-
return {
|
|
255
|
-
result: updated,
|
|
256
|
-
changes: changeset,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
finally {
|
|
260
|
-
this.expressions.setCollector(null);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
removeExpression(expressionId, deleteSubtree) {
|
|
264
|
-
// Snapshot the expression before removal (for result).
|
|
265
|
-
const snapshot = this.expressions.getExpression(expressionId);
|
|
266
|
-
const collector = new ChangeCollector();
|
|
267
|
-
this.expressions.setCollector(collector);
|
|
268
|
-
try {
|
|
269
|
-
if (!snapshot) {
|
|
270
291
|
return {
|
|
271
|
-
result:
|
|
272
|
-
changes
|
|
292
|
+
result: this.expressions.getExpression(expression.id),
|
|
293
|
+
changes,
|
|
273
294
|
};
|
|
274
295
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
finally {
|
|
297
|
+
this.expressions.setCollector(null);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
updateExpression(expressionId, updates) {
|
|
302
|
+
return this.withValidation(() => {
|
|
303
|
+
const existing = this.expressions.getExpression(expressionId);
|
|
304
|
+
if (!existing) {
|
|
305
|
+
throw new Error(`Expression "${expressionId}" not found in premise "${this.premise.id}".`);
|
|
306
|
+
}
|
|
307
|
+
if (updates.variableId !== undefined) {
|
|
308
|
+
if (!this.variables.hasVariable(updates.variableId)) {
|
|
309
|
+
throw new Error(`Variable expression "${expressionId}" references non-existent variable "${updates.variableId}".`);
|
|
287
310
|
}
|
|
288
311
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
312
|
+
const collector = new ChangeCollector();
|
|
313
|
+
this.expressions.setCollector(collector);
|
|
314
|
+
try {
|
|
315
|
+
const oldVariableId = existing.type === "variable"
|
|
316
|
+
? existing.variableId
|
|
317
|
+
: undefined;
|
|
318
|
+
const updated = this.expressions.updateExpression(expressionId, updates);
|
|
319
|
+
if (updates.variableId !== undefined &&
|
|
320
|
+
oldVariableId !== undefined &&
|
|
321
|
+
oldVariableId !== updates.variableId) {
|
|
293
322
|
this.expressionsByVariableId
|
|
294
|
-
.get(
|
|
295
|
-
?.delete(
|
|
323
|
+
.get(oldVariableId)
|
|
324
|
+
?.delete(expressionId);
|
|
325
|
+
this.expressionsByVariableId
|
|
326
|
+
.get(updates.variableId)
|
|
327
|
+
.add(expressionId);
|
|
296
328
|
}
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
this.expressions.setCollector(null);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
insertExpression(expression, leftNodeId, rightNodeId) {
|
|
314
|
-
this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
|
|
315
|
-
if (expression.type === "variable" &&
|
|
316
|
-
!this.variables.hasVariable(expression.variableId)) {
|
|
317
|
-
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
318
|
-
}
|
|
319
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
320
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
321
|
-
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
329
|
+
const changeset = this.flushAndBuildChangeset(collector);
|
|
330
|
+
if (changeset.expressions !== undefined) {
|
|
331
|
+
this.markDirty();
|
|
332
|
+
this.onMutate?.();
|
|
333
|
+
}
|
|
334
|
+
this.syncExpressionIndex(changeset);
|
|
335
|
+
return {
|
|
336
|
+
result: updated,
|
|
337
|
+
changes: changeset,
|
|
338
|
+
};
|
|
322
339
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.expressions.setCollector(collector);
|
|
326
|
-
try {
|
|
327
|
-
this.expressions.insertExpression(expression, leftNodeId, rightNodeId);
|
|
328
|
-
if (expression.type === "variable") {
|
|
329
|
-
this.expressionsByVariableId
|
|
330
|
-
.get(expression.variableId)
|
|
331
|
-
.add(expression.id);
|
|
340
|
+
finally {
|
|
341
|
+
this.expressions.setCollector(null);
|
|
332
342
|
}
|
|
333
|
-
|
|
334
|
-
this.markDirty();
|
|
335
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
336
|
-
this.syncExpressionIndex(changes);
|
|
337
|
-
this.onMutate?.();
|
|
338
|
-
return {
|
|
339
|
-
result: this.expressions.getExpression(expression.id),
|
|
340
|
-
changes,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
finally {
|
|
344
|
-
this.expressions.setCollector(null);
|
|
345
|
-
}
|
|
343
|
+
});
|
|
346
344
|
}
|
|
347
|
-
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
345
|
+
removeExpression(expressionId, deleteSubtree) {
|
|
346
|
+
return this.withValidation(() => {
|
|
347
|
+
// Snapshot the expression before removal (for result).
|
|
348
|
+
const snapshot = this.expressions.getExpression(expressionId);
|
|
349
|
+
const collector = new ChangeCollector();
|
|
350
|
+
this.expressions.setCollector(collector);
|
|
351
|
+
try {
|
|
352
|
+
if (!snapshot) {
|
|
353
|
+
return {
|
|
354
|
+
result: undefined,
|
|
355
|
+
changes: collector.toChangeset(),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (deleteSubtree) {
|
|
359
|
+
// Snapshot the subtree before deletion so we can clean up
|
|
360
|
+
// expressionsByVariableId for cascade-deleted descendants — they are
|
|
361
|
+
// not individually surfaced by ExpressionManager.removeExpression.
|
|
362
|
+
const subtree = this.collectSubtree(expressionId);
|
|
363
|
+
this.expressions.removeExpression(expressionId, true);
|
|
364
|
+
for (const expr of subtree) {
|
|
365
|
+
if (expr.type === "variable") {
|
|
366
|
+
this.expressionsByVariableId
|
|
367
|
+
.get(expr.variableId)
|
|
368
|
+
?.delete(expr.id);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Only clean up expressionsByVariableId for the removed
|
|
374
|
+
// expression itself — children survive promotion.
|
|
375
|
+
if (snapshot.type === "variable") {
|
|
376
|
+
this.expressionsByVariableId
|
|
377
|
+
.get(snapshot.variableId)
|
|
378
|
+
?.delete(snapshot.id);
|
|
379
|
+
}
|
|
380
|
+
this.expressions.removeExpression(expressionId, false);
|
|
381
|
+
}
|
|
382
|
+
this.syncRootExpressionId();
|
|
383
|
+
this.markDirty();
|
|
384
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
385
|
+
this.syncExpressionIndex(changes);
|
|
386
|
+
this.onMutate?.();
|
|
387
|
+
return {
|
|
388
|
+
result: snapshot,
|
|
389
|
+
changes,
|
|
390
|
+
};
|
|
357
391
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.expressions.setCollector(collector);
|
|
361
|
-
try {
|
|
362
|
-
this.expressions.wrapExpression(operator, newSibling, leftNodeId, rightNodeId);
|
|
363
|
-
if (newSibling.type === "variable") {
|
|
364
|
-
this.expressionsByVariableId
|
|
365
|
-
.get(newSibling.variableId)
|
|
366
|
-
.add(newSibling.id);
|
|
392
|
+
finally {
|
|
393
|
+
this.expressions.setCollector(null);
|
|
367
394
|
}
|
|
368
|
-
|
|
369
|
-
this.markDirty();
|
|
370
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
371
|
-
this.syncExpressionIndex(changes);
|
|
372
|
-
this.onMutate?.();
|
|
373
|
-
return {
|
|
374
|
-
result: this.expressions.getExpression(operator.id),
|
|
375
|
-
changes,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
finally {
|
|
379
|
-
this.expressions.setCollector(null);
|
|
380
|
-
}
|
|
395
|
+
});
|
|
381
396
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
? this.expressions.getExpression(target.parentId)
|
|
393
|
-
: undefined;
|
|
394
|
-
// Check for direct not parent: not(target)
|
|
395
|
-
const isDirectNot = parent?.type === "operator" && parent.operator === "not";
|
|
396
|
-
// Check for formula-buffered not: not(formula(target))
|
|
397
|
-
const grandparent = parent?.type === "formula" && parent.parentId
|
|
398
|
-
? this.expressions.getExpression(parent.parentId)
|
|
399
|
-
: undefined;
|
|
400
|
-
const isBufferedNot = parent?.type === "formula" &&
|
|
401
|
-
grandparent?.type === "operator" &&
|
|
402
|
-
grandparent.operator === "not";
|
|
403
|
-
if (isDirectNot || isBufferedNot) {
|
|
404
|
-
if (isBufferedNot) {
|
|
405
|
-
// Structure is not → formula → target.
|
|
406
|
-
// Remove just the not (promotes formula into its slot).
|
|
407
|
-
// The formula remains as a transparent wrapper.
|
|
408
|
-
this.expressions.removeExpression(grandparent.id, false);
|
|
397
|
+
insertExpression(expression, leftNodeId, rightNodeId) {
|
|
398
|
+
return this.withValidation(() => {
|
|
399
|
+
this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
|
|
400
|
+
if (expression.type === "variable" &&
|
|
401
|
+
!this.variables.hasVariable(expression.variableId)) {
|
|
402
|
+
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
403
|
+
}
|
|
404
|
+
if (expression.type === "variable" && this.circularityCheck) {
|
|
405
|
+
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
406
|
+
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
409
407
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
408
|
+
}
|
|
409
|
+
const collector = new ChangeCollector();
|
|
410
|
+
this.expressions.setCollector(collector);
|
|
411
|
+
try {
|
|
412
|
+
this.expressions.insertExpression(expression, leftNodeId, rightNodeId);
|
|
413
|
+
if (expression.type === "variable") {
|
|
414
|
+
this.expressionsByVariableId
|
|
415
|
+
.get(expression.variableId)
|
|
416
|
+
.add(expression.id);
|
|
413
417
|
}
|
|
414
418
|
this.syncRootExpressionId();
|
|
415
419
|
this.markDirty();
|
|
416
420
|
const changes = this.flushAndBuildChangeset(collector);
|
|
417
421
|
this.syncExpressionIndex(changes);
|
|
418
422
|
this.onMutate?.();
|
|
419
|
-
return {
|
|
423
|
+
return {
|
|
424
|
+
result: this.expressions.getExpression(expression.id),
|
|
425
|
+
changes,
|
|
426
|
+
};
|
|
420
427
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
position: target.position,
|
|
438
|
-
};
|
|
439
|
-
this.expressions.insertExpression(formulaExpr, expressionId);
|
|
440
|
-
const notExpr = {
|
|
441
|
-
...extraFields,
|
|
442
|
-
id: randomUUID(),
|
|
443
|
-
argumentId: target.argumentId,
|
|
444
|
-
argumentVersion: target.argumentVersion,
|
|
445
|
-
premiseId: target.premiseId,
|
|
446
|
-
type: "operator",
|
|
447
|
-
operator: "not",
|
|
448
|
-
parentId: target.parentId,
|
|
449
|
-
position: target.position,
|
|
450
|
-
};
|
|
451
|
-
this.expressions.insertExpression(notExpr, formulaExpr.id);
|
|
452
|
-
notExprId = notExpr.id;
|
|
428
|
+
finally {
|
|
429
|
+
this.expressions.setCollector(null);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
|
|
434
|
+
return this.withValidation(() => {
|
|
435
|
+
this.assertBelongsToArgument(operator.argumentId, operator.argumentVersion);
|
|
436
|
+
this.assertBelongsToArgument(newSibling.argumentId, newSibling.argumentVersion);
|
|
437
|
+
if (newSibling.type === "variable" &&
|
|
438
|
+
!this.variables.hasVariable(newSibling.variableId)) {
|
|
439
|
+
throw new Error(`Variable expression "${newSibling.id}" references non-existent variable "${newSibling.variableId}".`);
|
|
440
|
+
}
|
|
441
|
+
if (newSibling.type === "variable" && this.circularityCheck) {
|
|
442
|
+
if (this.circularityCheck(newSibling.variableId, this.premise.id)) {
|
|
443
|
+
throw new Error(`Circular binding: variable "${newSibling.variableId}" is bound to this premise (directly or transitively)`);
|
|
453
444
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
operator: "not",
|
|
464
|
-
parentId: target.parentId,
|
|
465
|
-
position: target.position,
|
|
466
|
-
};
|
|
467
|
-
this.expressions.insertExpression(notExpr, expressionId);
|
|
468
|
-
notExprId = notExpr.id;
|
|
445
|
+
}
|
|
446
|
+
const collector = new ChangeCollector();
|
|
447
|
+
this.expressions.setCollector(collector);
|
|
448
|
+
try {
|
|
449
|
+
this.expressions.wrapExpression(operator, newSibling, leftNodeId, rightNodeId);
|
|
450
|
+
if (newSibling.type === "variable") {
|
|
451
|
+
this.expressionsByVariableId
|
|
452
|
+
.get(newSibling.variableId)
|
|
453
|
+
.add(newSibling.id);
|
|
469
454
|
}
|
|
470
455
|
this.syncRootExpressionId();
|
|
471
456
|
this.markDirty();
|
|
@@ -473,87 +458,47 @@ export class PremiseEngine {
|
|
|
473
458
|
this.syncExpressionIndex(changes);
|
|
474
459
|
this.onMutate?.();
|
|
475
460
|
return {
|
|
476
|
-
result: this.expressions.getExpression(
|
|
461
|
+
result: this.expressions.getExpression(operator.id),
|
|
477
462
|
changes,
|
|
478
463
|
};
|
|
479
464
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
465
|
+
finally {
|
|
466
|
+
this.expressions.setCollector(null);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
484
469
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
this.assertBelongsToArgument(target.argumentId, target.argumentVersion);
|
|
497
|
-
// No-op: already the requested operator
|
|
498
|
-
if (target.type === "operator" && target.operator === newOperator) {
|
|
499
|
-
return { result: target, changes: {} };
|
|
500
|
-
}
|
|
501
|
-
const children = this.expressions.getChildExpressions(expressionId);
|
|
502
|
-
const childCount = children.length;
|
|
503
|
-
const collector = new ChangeCollector();
|
|
504
|
-
this.expressions.setCollector(collector);
|
|
505
|
-
try {
|
|
506
|
-
if (childCount <= 2) {
|
|
507
|
-
// Check for merge condition: parent is same type as newOperator
|
|
470
|
+
toggleNegation(expressionId, extraFields) {
|
|
471
|
+
return this.withValidation(() => {
|
|
472
|
+
const target = this.expressions.getExpression(expressionId);
|
|
473
|
+
if (!target) {
|
|
474
|
+
throw new Error(`Expression "${expressionId}" not found in this premise.`);
|
|
475
|
+
}
|
|
476
|
+
this.assertBelongsToArgument(target.argumentId, target.argumentVersion);
|
|
477
|
+
const collector = new ChangeCollector();
|
|
478
|
+
this.expressions.setCollector(collector);
|
|
479
|
+
try {
|
|
508
480
|
const parent = target.parentId
|
|
509
481
|
? this.expressions.getExpression(target.parentId)
|
|
510
482
|
: undefined;
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// Reparent children of the dissolving operator under the merge target.
|
|
527
|
-
// Use the dissolving operator's position slot for the first child,
|
|
528
|
-
// compute midpoint positions for subsequent children.
|
|
529
|
-
// If parent was a formula buffer, we'll dissolve that too
|
|
530
|
-
const formulaToDissolve = parent?.type === "formula" ? parent : undefined;
|
|
531
|
-
// The position slot we're replacing
|
|
532
|
-
const slotPosition = formulaToDissolve
|
|
533
|
-
? formulaToDissolve.position
|
|
534
|
-
: target.position;
|
|
535
|
-
// Get the merge target's existing children sorted by position to find neighbors
|
|
536
|
-
const mergeChildren = this.expressions.getChildExpressions(mergeTarget.id);
|
|
537
|
-
// Find the position of the next sibling after the slot
|
|
538
|
-
const slotIndex = mergeChildren.findIndex((c) => c.id === (formulaToDissolve?.id ?? expressionId));
|
|
539
|
-
const nextSibling = mergeChildren[slotIndex + 1];
|
|
540
|
-
const nextPosition = nextSibling
|
|
541
|
-
? nextSibling.position
|
|
542
|
-
: POSITION_MAX;
|
|
543
|
-
// Reparent each child
|
|
544
|
-
for (let i = 0; i < children.length; i++) {
|
|
545
|
-
const childPosition = i === 0
|
|
546
|
-
? slotPosition
|
|
547
|
-
: midpoint(i === 1
|
|
548
|
-
? slotPosition
|
|
549
|
-
: children[i - 1].position, nextPosition);
|
|
550
|
-
this.expressions.reparentExpression(children[i].id, mergeTarget.id, childPosition);
|
|
483
|
+
// Check for direct not parent: not(target)
|
|
484
|
+
const isDirectNot = parent?.type === "operator" && parent.operator === "not";
|
|
485
|
+
// Check for formula-buffered not: not(formula(target))
|
|
486
|
+
const grandparent = parent?.type === "formula" && parent.parentId
|
|
487
|
+
? this.expressions.getExpression(parent.parentId)
|
|
488
|
+
: undefined;
|
|
489
|
+
const isBufferedNot = parent?.type === "formula" &&
|
|
490
|
+
grandparent?.type === "operator" &&
|
|
491
|
+
grandparent.operator === "not";
|
|
492
|
+
if (isDirectNot || isBufferedNot) {
|
|
493
|
+
if (isBufferedNot) {
|
|
494
|
+
// Structure is not → formula → target.
|
|
495
|
+
// Remove just the not (promotes formula into its slot).
|
|
496
|
+
// The formula remains as a transparent wrapper.
|
|
497
|
+
this.expressions.removeExpression(grandparent.id, false);
|
|
551
498
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (formulaToDissolve) {
|
|
556
|
-
this.expressions.deleteExpression(formulaToDissolve.id);
|
|
499
|
+
else {
|
|
500
|
+
// Remove the NOT operator, promoting target into its slot
|
|
501
|
+
this.expressions.removeExpression(parent.id, false);
|
|
557
502
|
}
|
|
558
503
|
this.syncRootExpressionId();
|
|
559
504
|
this.markDirty();
|
|
@@ -563,96 +508,243 @@ export class PremiseEngine {
|
|
|
563
508
|
return { result: null, changes };
|
|
564
509
|
}
|
|
565
510
|
else {
|
|
566
|
-
//
|
|
567
|
-
|
|
511
|
+
// When the target is a non-not operator, insert a formula
|
|
512
|
+
// buffer between the new not and the target so the tree
|
|
513
|
+
// satisfies the operator nesting restriction.
|
|
514
|
+
const needsFormula = target.type === "operator" && target.operator !== "not";
|
|
515
|
+
let notExprId;
|
|
516
|
+
if (needsFormula) {
|
|
517
|
+
// Build not → formula → target
|
|
518
|
+
const formulaExpr = {
|
|
519
|
+
...extraFields,
|
|
520
|
+
id: randomUUID(),
|
|
521
|
+
argumentId: target.argumentId,
|
|
522
|
+
argumentVersion: target.argumentVersion,
|
|
523
|
+
premiseId: target.premiseId,
|
|
524
|
+
type: "formula",
|
|
525
|
+
parentId: target.parentId,
|
|
526
|
+
position: target.position,
|
|
527
|
+
};
|
|
528
|
+
this.expressions.insertExpression(formulaExpr, expressionId);
|
|
529
|
+
const notExpr = {
|
|
530
|
+
...extraFields,
|
|
531
|
+
id: randomUUID(),
|
|
532
|
+
argumentId: target.argumentId,
|
|
533
|
+
argumentVersion: target.argumentVersion,
|
|
534
|
+
premiseId: target.premiseId,
|
|
535
|
+
type: "operator",
|
|
536
|
+
operator: "not",
|
|
537
|
+
parentId: target.parentId,
|
|
538
|
+
position: target.position,
|
|
539
|
+
};
|
|
540
|
+
this.expressions.insertExpression(notExpr, formulaExpr.id);
|
|
541
|
+
notExprId = notExpr.id;
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
// Wrap target with a new NOT operator
|
|
545
|
+
const notExpr = {
|
|
546
|
+
...extraFields,
|
|
547
|
+
id: randomUUID(),
|
|
548
|
+
argumentId: target.argumentId,
|
|
549
|
+
argumentVersion: target.argumentVersion,
|
|
550
|
+
premiseId: target.premiseId,
|
|
551
|
+
type: "operator",
|
|
552
|
+
operator: "not",
|
|
553
|
+
parentId: target.parentId,
|
|
554
|
+
position: target.position,
|
|
555
|
+
};
|
|
556
|
+
this.expressions.insertExpression(notExpr, expressionId);
|
|
557
|
+
notExprId = notExpr.id;
|
|
558
|
+
}
|
|
568
559
|
this.syncRootExpressionId();
|
|
569
560
|
this.markDirty();
|
|
570
561
|
const changes = this.flushAndBuildChangeset(collector);
|
|
571
562
|
this.syncExpressionIndex(changes);
|
|
572
563
|
this.onMutate?.();
|
|
573
564
|
return {
|
|
574
|
-
result: this.expressions.getExpression(
|
|
565
|
+
result: this.expressions.getExpression(notExprId),
|
|
575
566
|
changes,
|
|
576
567
|
};
|
|
577
568
|
}
|
|
578
569
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
570
|
+
finally {
|
|
571
|
+
this.expressions.setCollector(null);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
changeOperator(expressionId, newOperator, sourceChildId, targetChildId, extraFields) {
|
|
576
|
+
return this.withValidation(() => {
|
|
577
|
+
const target = this.expressions.getExpression(expressionId);
|
|
578
|
+
if (!target) {
|
|
579
|
+
throw new Error(`Expression "${expressionId}" not found in this premise.`);
|
|
580
|
+
}
|
|
581
|
+
if (target.type !== "operator") {
|
|
582
|
+
throw new Error(`Expression "${expressionId}" is not an operator expression (type: "${target.type}").`);
|
|
583
|
+
}
|
|
584
|
+
if (target.type === "operator" && target.operator === "not") {
|
|
585
|
+
throw new Error(`Cannot change a "not" operator. Use toggleNegation instead.`);
|
|
586
|
+
}
|
|
587
|
+
this.assertBelongsToArgument(target.argumentId, target.argumentVersion);
|
|
588
|
+
// No-op: already the requested operator
|
|
589
|
+
if (target.type === "operator" && target.operator === newOperator) {
|
|
590
|
+
return { result: target, changes: {} };
|
|
591
|
+
}
|
|
592
|
+
const children = this.expressions.getChildExpressions(expressionId);
|
|
593
|
+
const childCount = children.length;
|
|
594
|
+
const collector = new ChangeCollector();
|
|
595
|
+
this.expressions.setCollector(collector);
|
|
596
|
+
try {
|
|
597
|
+
if (childCount <= 2) {
|
|
598
|
+
// Check for merge condition: parent is same type as newOperator
|
|
599
|
+
const parent = target.parentId
|
|
600
|
+
? this.expressions.getExpression(target.parentId)
|
|
601
|
+
: undefined;
|
|
602
|
+
// Look through formula buffer: if parent is formula, check grandparent
|
|
603
|
+
let mergeTarget;
|
|
604
|
+
if (parent?.type === "formula" && parent.parentId) {
|
|
605
|
+
const grandparent = this.expressions.getExpression(parent.parentId);
|
|
606
|
+
if (grandparent?.type === "operator" &&
|
|
607
|
+
grandparent.operator === newOperator) {
|
|
608
|
+
mergeTarget = grandparent;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else if (parent?.type === "operator" &&
|
|
612
|
+
parent.operator === newOperator) {
|
|
613
|
+
mergeTarget = parent;
|
|
614
|
+
}
|
|
615
|
+
if (mergeTarget) {
|
|
616
|
+
// --- MERGE ---
|
|
617
|
+
// Reparent children of the dissolving operator under the merge target.
|
|
618
|
+
// Use the dissolving operator's position slot for the first child,
|
|
619
|
+
// compute midpoint positions for subsequent children.
|
|
620
|
+
// If parent was a formula buffer, we'll dissolve that too
|
|
621
|
+
const formulaToDissolve = parent?.type === "formula" ? parent : undefined;
|
|
622
|
+
// The position slot we're replacing
|
|
623
|
+
const slotPosition = formulaToDissolve
|
|
624
|
+
? formulaToDissolve.position
|
|
625
|
+
: target.position;
|
|
626
|
+
// Get the merge target's existing children sorted by position to find neighbors
|
|
627
|
+
const mergeChildren = this.expressions.getChildExpressions(mergeTarget.id);
|
|
628
|
+
// Find the position of the next sibling after the slot
|
|
629
|
+
const slotIndex = mergeChildren.findIndex((c) => c.id === (formulaToDissolve?.id ?? expressionId));
|
|
630
|
+
const nextSibling = mergeChildren[slotIndex + 1];
|
|
631
|
+
const nextPosition = nextSibling
|
|
632
|
+
? nextSibling.position
|
|
633
|
+
: POSITION_MAX;
|
|
634
|
+
// Reparent each child
|
|
635
|
+
for (let i = 0; i < children.length; i++) {
|
|
636
|
+
const childPosition = i === 0
|
|
637
|
+
? slotPosition
|
|
638
|
+
: midpoint(i === 1
|
|
639
|
+
? slotPosition
|
|
640
|
+
: children[i - 1].position, nextPosition);
|
|
641
|
+
this.expressions.reparentExpression(children[i].id, mergeTarget.id, childPosition);
|
|
642
|
+
}
|
|
643
|
+
// Delete the dissolving operator (now has no children)
|
|
644
|
+
this.expressions.deleteExpression(expressionId);
|
|
645
|
+
// Delete the formula buffer if it existed (now has no children)
|
|
646
|
+
if (formulaToDissolve) {
|
|
647
|
+
this.expressions.deleteExpression(formulaToDissolve.id);
|
|
648
|
+
}
|
|
649
|
+
this.syncRootExpressionId();
|
|
650
|
+
this.markDirty();
|
|
651
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
652
|
+
this.syncExpressionIndex(changes);
|
|
653
|
+
this.onMutate?.();
|
|
654
|
+
return { result: null, changes };
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
// --- SIMPLE CHANGE ---
|
|
658
|
+
this.expressions.changeOperatorType(expressionId, newOperator);
|
|
659
|
+
this.syncRootExpressionId();
|
|
660
|
+
this.markDirty();
|
|
661
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
662
|
+
this.syncExpressionIndex(changes);
|
|
663
|
+
this.onMutate?.();
|
|
664
|
+
return {
|
|
665
|
+
result: this.expressions.getExpression(expressionId),
|
|
666
|
+
changes,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
589
669
|
}
|
|
590
|
-
|
|
591
|
-
|
|
670
|
+
else {
|
|
671
|
+
// --- SPLIT (>2 children) ---
|
|
672
|
+
if (!sourceChildId || !targetChildId) {
|
|
673
|
+
throw new Error(`Operator "${expressionId}" has ${childCount} children — sourceChildId and targetChildId are required for split.`);
|
|
674
|
+
}
|
|
675
|
+
// Validate source and target are children of the operator
|
|
676
|
+
const sourceChild = this.expressions.getExpression(sourceChildId);
|
|
677
|
+
const targetChild = this.expressions.getExpression(targetChildId);
|
|
678
|
+
if (!sourceChild || sourceChild.parentId !== expressionId) {
|
|
679
|
+
throw new Error(`Expression "${sourceChildId}" is not a child of operator "${expressionId}".`);
|
|
680
|
+
}
|
|
681
|
+
if (!targetChild || targetChild.parentId !== expressionId) {
|
|
682
|
+
throw new Error(`Expression "${targetChildId}" is not a child of operator "${expressionId}".`);
|
|
683
|
+
}
|
|
684
|
+
// Determine position for the formula buffer (min of the two children)
|
|
685
|
+
const formulaPosition = Math.min(sourceChild.position, targetChild.position);
|
|
686
|
+
// Create the sub-operator and formula first as detached nodes,
|
|
687
|
+
// then reparent children away from the parent (freeing their
|
|
688
|
+
// position slots), and finally add formula + sub-operator.
|
|
689
|
+
const formulaId = randomUUID();
|
|
690
|
+
const newOpId = randomUUID();
|
|
691
|
+
// Reparent source and target children to a temporary holding
|
|
692
|
+
// position under the new sub-operator. We must reparent them
|
|
693
|
+
// away from the parent BEFORE adding the formula at their old
|
|
694
|
+
// position slot.
|
|
695
|
+
const firstChild = sourceChild.position <= targetChild.position
|
|
696
|
+
? sourceChild
|
|
697
|
+
: targetChild;
|
|
698
|
+
const secondChild = sourceChild.position <= targetChild.position
|
|
699
|
+
? targetChild
|
|
700
|
+
: sourceChild;
|
|
701
|
+
// Reparent children to null temporarily (detach from parent)
|
|
702
|
+
// so their position slots are freed.
|
|
703
|
+
this.expressions.reparentExpression(firstChild.id, null, firstChild.position);
|
|
704
|
+
this.expressions.reparentExpression(secondChild.id, null, secondChild.position);
|
|
705
|
+
// Now add the formula buffer at the freed position
|
|
706
|
+
const formulaExpr = {
|
|
707
|
+
...extraFields,
|
|
708
|
+
id: formulaId,
|
|
709
|
+
argumentId: target.argumentId,
|
|
710
|
+
argumentVersion: target.argumentVersion,
|
|
711
|
+
premiseId: target.premiseId,
|
|
712
|
+
type: "formula",
|
|
713
|
+
parentId: expressionId,
|
|
714
|
+
position: formulaPosition,
|
|
715
|
+
};
|
|
716
|
+
this.expressions.addExpression(formulaExpr);
|
|
717
|
+
// Add the new sub-operator under the formula
|
|
718
|
+
const newOpExpr = {
|
|
719
|
+
...extraFields,
|
|
720
|
+
id: newOpId,
|
|
721
|
+
argumentId: target.argumentId,
|
|
722
|
+
argumentVersion: target.argumentVersion,
|
|
723
|
+
premiseId: target.premiseId,
|
|
724
|
+
type: "operator",
|
|
725
|
+
operator: newOperator,
|
|
726
|
+
parentId: formulaId,
|
|
727
|
+
position: POSITION_INITIAL,
|
|
728
|
+
};
|
|
729
|
+
this.expressions.addExpression(newOpExpr);
|
|
730
|
+
// Now reparent the children under the new sub-operator
|
|
731
|
+
this.expressions.reparentExpression(firstChild.id, newOpId, POSITION_INITIAL);
|
|
732
|
+
this.expressions.reparentExpression(secondChild.id, newOpId, midpoint(POSITION_INITIAL, POSITION_MAX));
|
|
733
|
+
this.syncRootExpressionId();
|
|
734
|
+
this.markDirty();
|
|
735
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
736
|
+
this.syncExpressionIndex(changes);
|
|
737
|
+
this.onMutate?.();
|
|
738
|
+
return {
|
|
739
|
+
result: this.expressions.getExpression(newOpId),
|
|
740
|
+
changes,
|
|
741
|
+
};
|
|
592
742
|
}
|
|
593
|
-
// Determine position for the formula buffer (min of the two children)
|
|
594
|
-
const formulaPosition = Math.min(sourceChild.position, targetChild.position);
|
|
595
|
-
// Create the sub-operator and formula first as detached nodes,
|
|
596
|
-
// then reparent children away from the parent (freeing their
|
|
597
|
-
// position slots), and finally add formula + sub-operator.
|
|
598
|
-
const formulaId = randomUUID();
|
|
599
|
-
const newOpId = randomUUID();
|
|
600
|
-
// Reparent source and target children to a temporary holding
|
|
601
|
-
// position under the new sub-operator. We must reparent them
|
|
602
|
-
// away from the parent BEFORE adding the formula at their old
|
|
603
|
-
// position slot.
|
|
604
|
-
const firstChild = sourceChild.position <= targetChild.position
|
|
605
|
-
? sourceChild
|
|
606
|
-
: targetChild;
|
|
607
|
-
const secondChild = sourceChild.position <= targetChild.position
|
|
608
|
-
? targetChild
|
|
609
|
-
: sourceChild;
|
|
610
|
-
// Reparent children to null temporarily (detach from parent)
|
|
611
|
-
// so their position slots are freed.
|
|
612
|
-
this.expressions.reparentExpression(firstChild.id, null, firstChild.position);
|
|
613
|
-
this.expressions.reparentExpression(secondChild.id, null, secondChild.position);
|
|
614
|
-
// Now add the formula buffer at the freed position
|
|
615
|
-
const formulaExpr = {
|
|
616
|
-
...extraFields,
|
|
617
|
-
id: formulaId,
|
|
618
|
-
argumentId: target.argumentId,
|
|
619
|
-
argumentVersion: target.argumentVersion,
|
|
620
|
-
premiseId: target.premiseId,
|
|
621
|
-
type: "formula",
|
|
622
|
-
parentId: expressionId,
|
|
623
|
-
position: formulaPosition,
|
|
624
|
-
};
|
|
625
|
-
this.expressions.addExpression(formulaExpr);
|
|
626
|
-
// Add the new sub-operator under the formula
|
|
627
|
-
const newOpExpr = {
|
|
628
|
-
...extraFields,
|
|
629
|
-
id: newOpId,
|
|
630
|
-
argumentId: target.argumentId,
|
|
631
|
-
argumentVersion: target.argumentVersion,
|
|
632
|
-
premiseId: target.premiseId,
|
|
633
|
-
type: "operator",
|
|
634
|
-
operator: newOperator,
|
|
635
|
-
parentId: formulaId,
|
|
636
|
-
position: POSITION_INITIAL,
|
|
637
|
-
};
|
|
638
|
-
this.expressions.addExpression(newOpExpr);
|
|
639
|
-
// Now reparent the children under the new sub-operator
|
|
640
|
-
this.expressions.reparentExpression(firstChild.id, newOpId, POSITION_INITIAL);
|
|
641
|
-
this.expressions.reparentExpression(secondChild.id, newOpId, midpoint(POSITION_INITIAL, POSITION_MAX));
|
|
642
|
-
this.syncRootExpressionId();
|
|
643
|
-
this.markDirty();
|
|
644
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
645
|
-
this.syncExpressionIndex(changes);
|
|
646
|
-
this.onMutate?.();
|
|
647
|
-
return {
|
|
648
|
-
result: this.expressions.getExpression(newOpId),
|
|
649
|
-
changes,
|
|
650
|
-
};
|
|
651
743
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
744
|
+
finally {
|
|
745
|
+
this.expressions.setCollector(null);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
656
748
|
}
|
|
657
749
|
getExpression(id) {
|
|
658
750
|
return this.expressions.getExpression(id);
|
|
@@ -665,20 +757,24 @@ export class PremiseEngine {
|
|
|
665
757
|
return { ...extras };
|
|
666
758
|
}
|
|
667
759
|
setExtras(extras) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
760
|
+
return this.withValidation(() => {
|
|
761
|
+
// Strip old extras and replace with new ones
|
|
762
|
+
const { id, argumentId, argumentVersion, checksum, descendantChecksum, combinedChecksum, } = this.premise;
|
|
763
|
+
this.premise = {
|
|
764
|
+
...extras,
|
|
765
|
+
id,
|
|
766
|
+
argumentId,
|
|
767
|
+
argumentVersion,
|
|
768
|
+
...(checksum !== undefined ? { checksum } : {}),
|
|
769
|
+
...(descendantChecksum !== undefined
|
|
770
|
+
? { descendantChecksum }
|
|
771
|
+
: {}),
|
|
772
|
+
...(combinedChecksum !== undefined ? { combinedChecksum } : {}),
|
|
773
|
+
};
|
|
774
|
+
this.markDirty();
|
|
775
|
+
this.onMutate?.();
|
|
776
|
+
return { result: this.getExtras(), changes: {} };
|
|
777
|
+
});
|
|
682
778
|
}
|
|
683
779
|
getRootExpressionId() {
|
|
684
780
|
return this.rootExpressionId;
|
|
@@ -855,7 +951,9 @@ export class PremiseEngine {
|
|
|
855
951
|
let value;
|
|
856
952
|
if (options?.resolver) {
|
|
857
953
|
const variable = this.variables.getVariable(expression.variableId);
|
|
858
|
-
if (variable &&
|
|
954
|
+
if (variable &&
|
|
955
|
+
isPremiseBound(variable) &&
|
|
956
|
+
!isExternallyBound(variable, this.argument.id)) {
|
|
859
957
|
value = options.resolver(expression.variableId);
|
|
860
958
|
}
|
|
861
959
|
else {
|
|
@@ -1036,6 +1134,71 @@ export class PremiseEngine {
|
|
|
1036
1134
|
: computeHash(this.cachedMetaChecksum + this.cachedDescendantChecksum);
|
|
1037
1135
|
this.checksumDirty = false;
|
|
1038
1136
|
}
|
|
1137
|
+
validate() {
|
|
1138
|
+
const violations = [];
|
|
1139
|
+
const premiseId = this.premise.id;
|
|
1140
|
+
// 1. Schema check (use toPremiseData() to include computed checksums)
|
|
1141
|
+
const premiseData = this.toPremiseData();
|
|
1142
|
+
if (!Value.Check(CorePremiseSchema, premiseData)) {
|
|
1143
|
+
violations.push({
|
|
1144
|
+
code: PREMISE_SCHEMA_INVALID,
|
|
1145
|
+
message: `Premise "${premiseId}" does not conform to CorePremiseSchema.`,
|
|
1146
|
+
entityType: "premise",
|
|
1147
|
+
entityId: premiseId,
|
|
1148
|
+
premiseId,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
// 2. Delegate to expression-level validation, attaching premiseId
|
|
1152
|
+
const exprResult = this.expressions.validate();
|
|
1153
|
+
for (const v of exprResult.violations) {
|
|
1154
|
+
violations.push({ ...v, premiseId });
|
|
1155
|
+
}
|
|
1156
|
+
// 3. Root expression consistency
|
|
1157
|
+
if (this.rootExpressionId !== undefined) {
|
|
1158
|
+
const rootExpr = this.expressions.getExpression(this.rootExpressionId);
|
|
1159
|
+
if (!rootExpr) {
|
|
1160
|
+
violations.push({
|
|
1161
|
+
code: PREMISE_ROOT_EXPRESSION_INVALID,
|
|
1162
|
+
message: `Premise "${premiseId}" rootExpressionId "${this.rootExpressionId}" does not exist in expression store.`,
|
|
1163
|
+
entityType: "premise",
|
|
1164
|
+
entityId: premiseId,
|
|
1165
|
+
premiseId,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
else if (rootExpr.parentId !== null) {
|
|
1169
|
+
violations.push({
|
|
1170
|
+
code: PREMISE_ROOT_EXPRESSION_INVALID,
|
|
1171
|
+
message: `Premise "${premiseId}" rootExpressionId "${this.rootExpressionId}" has non-null parentId "${rootExpr.parentId}".`,
|
|
1172
|
+
entityType: "premise",
|
|
1173
|
+
entityId: premiseId,
|
|
1174
|
+
premiseId,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// 4. Variable references: every variable-type expression must
|
|
1179
|
+
// reference a variableId that exists in the argument's variable set
|
|
1180
|
+
if (this.variableIdsCallback) {
|
|
1181
|
+
const variableIds = this.variableIdsCallback();
|
|
1182
|
+
for (const expr of this.expressions.toArray()) {
|
|
1183
|
+
if (expr.type === "variable") {
|
|
1184
|
+
const varExpr = expr;
|
|
1185
|
+
if (!variableIds.has(varExpr.variableId)) {
|
|
1186
|
+
violations.push({
|
|
1187
|
+
code: PREMISE_VARIABLE_REF_NOT_FOUND,
|
|
1188
|
+
message: `Expression "${expr.id}" in premise "${premiseId}" references non-existent variable "${varExpr.variableId}".`,
|
|
1189
|
+
entityType: "expression",
|
|
1190
|
+
entityId: expr.id,
|
|
1191
|
+
premiseId,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
ok: violations.length === 0,
|
|
1199
|
+
violations,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1039
1202
|
// -------------------------------------------------------------------------
|
|
1040
1203
|
// Private helpers
|
|
1041
1204
|
// -------------------------------------------------------------------------
|