@polintpro/proposit-core 0.6.5 → 0.7.3
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 +194 -0
- package/dist/extensions/basics/schemata.d.ts +5 -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 +21 -2
- package/dist/lib/consts.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +51 -2
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +764 -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 +28 -0
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +458 -20
- package/dist/lib/core/expression-manager.js.map +1 -1
- 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/library.interfaces.d.ts +19 -0
- package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +53 -1
- 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 +12 -1
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +726 -382
- 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 +4 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +4 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemata/argument.d.ts +2 -0
- package/dist/lib/schemata/argument.d.ts.map +1 -1
- package/dist/lib/schemata/argument.js +6 -0
- package/dist/lib/schemata/argument.js.map +1 -1
- package/dist/lib/schemata/propositional.d.ts +41 -0
- package/dist/lib/schemata/propositional.d.ts.map +1 -1
- package/dist/lib/schemata/propositional.js +34 -0
- package/dist/lib/schemata/propositional.js.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 +32 -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 +46 -0
- package/dist/lib/types/validation.d.ts.map +1 -0
- package/dist/lib/types/validation.js +41 -0
- package/dist/lib/types/validation.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,12 +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
|
+
import { midpoint, POSITION_INITIAL, POSITION_MAX } from "../utils/position.js";
|
|
4
5
|
import { sortedCopyById, sortedUnique } from "../utils/collections.js";
|
|
5
6
|
import { kleeneAnd, kleeneIff, kleeneImplies, kleeneNot, kleeneOr, } from "./evaluation/kleene.js";
|
|
6
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";
|
|
7
10
|
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
8
11
|
import { ChangeCollector } from "./change-collector.js";
|
|
9
12
|
import { computeHash, entityChecksum } from "./checksum.js";
|
|
13
|
+
import { InvariantViolationError } from "./invariant-violation-error.js";
|
|
10
14
|
import { ExpressionManager } from "./expression-manager.js";
|
|
11
15
|
import { VariableManager } from "./variable-manager.js";
|
|
12
16
|
export class PremiseEngine {
|
|
@@ -25,6 +29,9 @@ export class PremiseEngine {
|
|
|
25
29
|
onMutate;
|
|
26
30
|
circularityCheck;
|
|
27
31
|
emptyBoundPremiseCheck;
|
|
32
|
+
variableIdsCallback;
|
|
33
|
+
argumentValidateCallback;
|
|
34
|
+
insideValidation = false;
|
|
28
35
|
constructor(premise, deps, config) {
|
|
29
36
|
this.premise = { ...premise };
|
|
30
37
|
this.argument = deps.argument;
|
|
@@ -44,442 +51,700 @@ export class PremiseEngine {
|
|
|
44
51
|
setEmptyBoundPremiseCheck(check) {
|
|
45
52
|
this.emptyBoundPremiseCheck = check;
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
68
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const e of changes.expressions.removed) {
|
|
69
|
-
collector.removedExpression(e);
|
|
70
|
-
}
|
|
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);
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const changes = collector.toChangeset();
|
|
76
|
-
this.syncExpressionIndex(changes);
|
|
77
|
-
// Restore and fire once if something was removed
|
|
78
|
-
this.onMutate = savedOnMutate;
|
|
79
|
-
if (removed.length > 0) {
|
|
80
|
-
this.onMutate?.();
|
|
87
|
+
for (const [exprId, premiseId] of snap.expressionIndexEntries) {
|
|
88
|
+
this.expressionIndex.set(exprId, premiseId);
|
|
81
89
|
}
|
|
82
|
-
return {
|
|
83
|
-
result: removed,
|
|
84
|
-
changes,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
this.onMutate = savedOnMutate;
|
|
89
|
-
throw e;
|
|
90
90
|
}
|
|
91
|
+
this.rebuildVariableIndex();
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
!this.variables.hasVariable(expression.variableId)) {
|
|
96
|
-
throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
|
|
93
|
+
withValidation(fn) {
|
|
94
|
+
if (this.insideValidation) {
|
|
95
|
+
return fn();
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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);
|
|
101
105
|
}
|
|
106
|
+
return result;
|
|
102
107
|
}
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (!(e instanceof InvariantViolationError)) {
|
|
110
|
+
this.restoreFromPremiseSnapshot(snap);
|
|
106
111
|
}
|
|
112
|
+
throw e;
|
|
107
113
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
|
|
111
|
-
}
|
|
114
|
+
finally {
|
|
115
|
+
this.insideValidation = false;
|
|
112
116
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
177
|
if (expression.parentId === null) {
|
|
120
|
-
this.rootExpressionId
|
|
178
|
+
if (this.rootExpressionId !== undefined) {
|
|
179
|
+
throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
|
|
180
|
+
}
|
|
121
181
|
}
|
|
122
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
182
|
+
else {
|
|
183
|
+
if (!this.expressions.getExpression(expression.parentId)) {
|
|
184
|
+
throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
|
|
185
|
+
}
|
|
126
186
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
});
|
|
139
214
|
}
|
|
140
215
|
appendExpression(parentId, expression) {
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
147
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
148
|
-
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (parentId === null) {
|
|
152
|
-
if (this.rootExpressionId !== undefined) {
|
|
153
|
-
throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
|
|
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}".`);
|
|
154
221
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
226
|
}
|
|
160
|
-
}
|
|
161
|
-
const collector = new ChangeCollector();
|
|
162
|
-
this.expressions.setCollector(collector);
|
|
163
|
-
try {
|
|
164
|
-
this.expressions.appendExpression(parentId, expression);
|
|
165
227
|
if (parentId === null) {
|
|
166
|
-
this.
|
|
228
|
+
if (this.rootExpressionId !== undefined) {
|
|
229
|
+
throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
|
|
230
|
+
}
|
|
167
231
|
}
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
|
|
171
|
-
|
|
232
|
+
else {
|
|
233
|
+
if (!this.expressions.getExpression(parentId)) {
|
|
234
|
+
throw new Error(`Parent expression "${parentId}" does not exist in this premise.`);
|
|
235
|
+
}
|
|
172
236
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
});
|
|
185
262
|
}
|
|
186
263
|
addExpressionRelative(siblingId, relativePosition, expression) {
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
193
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
194
|
-
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}".`);
|
|
195
269
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const collector = new ChangeCollector();
|
|
201
|
-
this.expressions.setCollector(collector);
|
|
202
|
-
try {
|
|
203
|
-
this.expressions.addExpressionRelative(siblingId, relativePosition, expression);
|
|
204
|
-
if (expression.type === "variable") {
|
|
205
|
-
this.expressionsByVariableId
|
|
206
|
-
.get(expression.variableId)
|
|
207
|
-
.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
|
+
}
|
|
208
274
|
}
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
this.syncExpressionIndex(changes);
|
|
212
|
-
this.onMutate?.();
|
|
213
|
-
return {
|
|
214
|
-
result: this.expressions.getExpression(expression.id),
|
|
215
|
-
changes,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
this.expressions.setCollector(null);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
updateExpression(expressionId, updates) {
|
|
223
|
-
const existing = this.expressions.getExpression(expressionId);
|
|
224
|
-
if (!existing) {
|
|
225
|
-
throw new Error(`Expression "${expressionId}" not found in premise "${this.premise.id}".`);
|
|
226
|
-
}
|
|
227
|
-
if (updates.variableId !== undefined) {
|
|
228
|
-
if (!this.variables.hasVariable(updates.variableId)) {
|
|
229
|
-
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.`);
|
|
230
277
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.expressionsByVariableId
|
|
241
|
-
.get(oldVariableId)
|
|
242
|
-
?.delete(expressionId);
|
|
243
|
-
this.expressionsByVariableId
|
|
244
|
-
.get(updates.variableId)
|
|
245
|
-
.add(expressionId);
|
|
246
|
-
}
|
|
247
|
-
const changeset = this.flushAndBuildChangeset(collector);
|
|
248
|
-
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
|
+
}
|
|
249
287
|
this.markDirty();
|
|
288
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
289
|
+
this.syncExpressionIndex(changes);
|
|
250
290
|
this.onMutate?.();
|
|
291
|
+
return {
|
|
292
|
+
result: this.expressions.getExpression(expression.id),
|
|
293
|
+
changes,
|
|
294
|
+
};
|
|
251
295
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
finally {
|
|
259
|
-
this.expressions.setCollector(null);
|
|
260
|
-
}
|
|
296
|
+
finally {
|
|
297
|
+
this.expressions.setCollector(null);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
261
300
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (
|
|
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}".`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
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) {
|
|
322
|
+
this.expressionsByVariableId
|
|
323
|
+
.get(oldVariableId)
|
|
324
|
+
?.delete(expressionId);
|
|
325
|
+
this.expressionsByVariableId
|
|
326
|
+
.get(updates.variableId)
|
|
327
|
+
.add(expressionId);
|
|
328
|
+
}
|
|
329
|
+
const changeset = this.flushAndBuildChangeset(collector);
|
|
330
|
+
if (changeset.expressions !== undefined) {
|
|
331
|
+
this.markDirty();
|
|
332
|
+
this.onMutate?.();
|
|
333
|
+
}
|
|
334
|
+
this.syncExpressionIndex(changeset);
|
|
269
335
|
return {
|
|
270
|
-
result:
|
|
271
|
-
changes:
|
|
336
|
+
result: updated,
|
|
337
|
+
changes: changeset,
|
|
272
338
|
};
|
|
273
339
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
340
|
+
finally {
|
|
341
|
+
this.expressions.setCollector(null);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
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") {
|
|
282
376
|
this.expressionsByVariableId
|
|
283
|
-
.get(
|
|
284
|
-
?.delete(
|
|
377
|
+
.get(snapshot.variableId)
|
|
378
|
+
?.delete(snapshot.id);
|
|
285
379
|
}
|
|
380
|
+
this.expressions.removeExpression(expressionId, false);
|
|
286
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
|
+
};
|
|
287
391
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// expression itself — children survive promotion.
|
|
291
|
-
if (snapshot.type === "variable") {
|
|
292
|
-
this.expressionsByVariableId
|
|
293
|
-
.get(snapshot.variableId)
|
|
294
|
-
?.delete(snapshot.id);
|
|
295
|
-
}
|
|
296
|
-
this.expressions.removeExpression(expressionId, false);
|
|
392
|
+
finally {
|
|
393
|
+
this.expressions.setCollector(null);
|
|
297
394
|
}
|
|
298
|
-
|
|
299
|
-
this.markDirty();
|
|
300
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
301
|
-
this.syncExpressionIndex(changes);
|
|
302
|
-
this.onMutate?.();
|
|
303
|
-
return {
|
|
304
|
-
result: snapshot,
|
|
305
|
-
changes,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
finally {
|
|
309
|
-
this.expressions.setCollector(null);
|
|
310
|
-
}
|
|
395
|
+
});
|
|
311
396
|
}
|
|
312
397
|
insertExpression(expression, leftNodeId, rightNodeId) {
|
|
313
|
-
this.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (expression.type === "variable" && this.circularityCheck) {
|
|
319
|
-
if (this.circularityCheck(expression.variableId, this.premise.id)) {
|
|
320
|
-
throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
|
|
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}".`);
|
|
321
403
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this.expressions.insertExpression(expression, leftNodeId, rightNodeId);
|
|
327
|
-
if (expression.type === "variable") {
|
|
328
|
-
this.expressionsByVariableId
|
|
329
|
-
.get(expression.variableId)
|
|
330
|
-
.add(expression.id);
|
|
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)`);
|
|
407
|
+
}
|
|
331
408
|
}
|
|
332
|
-
|
|
333
|
-
this.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
!this.variables.hasVariable(newSibling.variableId)) {
|
|
351
|
-
throw new Error(`Variable expression "${newSibling.id}" references non-existent variable "${newSibling.variableId}".`);
|
|
352
|
-
}
|
|
353
|
-
if (newSibling.type === "variable" && this.circularityCheck) {
|
|
354
|
-
if (this.circularityCheck(newSibling.variableId, this.premise.id)) {
|
|
355
|
-
throw new Error(`Circular binding: variable "${newSibling.variableId}" is bound to this premise (directly or transitively)`);
|
|
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);
|
|
417
|
+
}
|
|
418
|
+
this.syncRootExpressionId();
|
|
419
|
+
this.markDirty();
|
|
420
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
421
|
+
this.syncExpressionIndex(changes);
|
|
422
|
+
this.onMutate?.();
|
|
423
|
+
return {
|
|
424
|
+
result: this.expressions.getExpression(expression.id),
|
|
425
|
+
changes,
|
|
426
|
+
};
|
|
356
427
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.expressions.setCollector(collector);
|
|
360
|
-
try {
|
|
361
|
-
this.expressions.wrapExpression(operator, newSibling, leftNodeId, rightNodeId);
|
|
362
|
-
if (newSibling.type === "variable") {
|
|
363
|
-
this.expressionsByVariableId
|
|
364
|
-
.get(newSibling.variableId)
|
|
365
|
-
.add(newSibling.id);
|
|
428
|
+
finally {
|
|
429
|
+
this.expressions.setCollector(null);
|
|
366
430
|
}
|
|
367
|
-
|
|
368
|
-
this.markDirty();
|
|
369
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
370
|
-
this.syncExpressionIndex(changes);
|
|
371
|
-
this.onMutate?.();
|
|
372
|
-
return {
|
|
373
|
-
result: this.expressions.getExpression(operator.id),
|
|
374
|
-
changes,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
finally {
|
|
378
|
-
this.expressions.setCollector(null);
|
|
379
|
-
}
|
|
431
|
+
});
|
|
380
432
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
: undefined;
|
|
393
|
-
// Check for direct not parent: not(target)
|
|
394
|
-
const isDirectNot = parent?.type === "operator" && parent.operator === "not";
|
|
395
|
-
// Check for formula-buffered not: not(formula(target))
|
|
396
|
-
const grandparent = parent?.type === "formula" && parent.parentId
|
|
397
|
-
? this.expressions.getExpression(parent.parentId)
|
|
398
|
-
: undefined;
|
|
399
|
-
const isBufferedNot = parent?.type === "formula" &&
|
|
400
|
-
grandparent?.type === "operator" &&
|
|
401
|
-
grandparent.operator === "not";
|
|
402
|
-
if (isDirectNot || isBufferedNot) {
|
|
403
|
-
if (isBufferedNot) {
|
|
404
|
-
// Structure is not → formula → target.
|
|
405
|
-
// Remove just the not (promotes formula into its slot).
|
|
406
|
-
// The formula remains as a transparent wrapper.
|
|
407
|
-
this.expressions.removeExpression(grandparent.id, false);
|
|
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)`);
|
|
408
444
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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);
|
|
412
454
|
}
|
|
413
455
|
this.syncRootExpressionId();
|
|
414
456
|
this.markDirty();
|
|
415
457
|
const changes = this.flushAndBuildChangeset(collector);
|
|
416
458
|
this.syncExpressionIndex(changes);
|
|
417
459
|
this.onMutate?.();
|
|
418
|
-
return {
|
|
460
|
+
return {
|
|
461
|
+
result: this.expressions.getExpression(operator.id),
|
|
462
|
+
changes,
|
|
463
|
+
};
|
|
419
464
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
465
|
+
finally {
|
|
466
|
+
this.expressions.setCollector(null);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
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 {
|
|
480
|
+
const parent = target.parentId
|
|
481
|
+
? this.expressions.getExpression(target.parentId)
|
|
482
|
+
: undefined;
|
|
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);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// Remove the NOT operator, promoting target into its slot
|
|
501
|
+
this.expressions.removeExpression(parent.id, false);
|
|
502
|
+
}
|
|
503
|
+
this.syncRootExpressionId();
|
|
504
|
+
this.markDirty();
|
|
505
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
506
|
+
this.syncExpressionIndex(changes);
|
|
507
|
+
this.onMutate?.();
|
|
508
|
+
return { result: null, changes };
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
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
|
+
}
|
|
559
|
+
this.syncRootExpressionId();
|
|
560
|
+
this.markDirty();
|
|
561
|
+
const changes = this.flushAndBuildChangeset(collector);
|
|
562
|
+
this.syncExpressionIndex(changes);
|
|
563
|
+
this.onMutate?.();
|
|
564
|
+
return {
|
|
565
|
+
result: this.expressions.getExpression(notExprId),
|
|
566
|
+
changes,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
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
|
+
}
|
|
669
|
+
}
|
|
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
|
|
428
706
|
const formulaExpr = {
|
|
429
707
|
...extraFields,
|
|
430
|
-
id:
|
|
708
|
+
id: formulaId,
|
|
431
709
|
argumentId: target.argumentId,
|
|
432
710
|
argumentVersion: target.argumentVersion,
|
|
433
711
|
premiseId: target.premiseId,
|
|
434
712
|
type: "formula",
|
|
435
|
-
parentId:
|
|
436
|
-
position:
|
|
713
|
+
parentId: expressionId,
|
|
714
|
+
position: formulaPosition,
|
|
437
715
|
};
|
|
438
|
-
this.expressions.
|
|
439
|
-
|
|
716
|
+
this.expressions.addExpression(formulaExpr);
|
|
717
|
+
// Add the new sub-operator under the formula
|
|
718
|
+
const newOpExpr = {
|
|
440
719
|
...extraFields,
|
|
441
|
-
id:
|
|
720
|
+
id: newOpId,
|
|
442
721
|
argumentId: target.argumentId,
|
|
443
722
|
argumentVersion: target.argumentVersion,
|
|
444
723
|
premiseId: target.premiseId,
|
|
445
724
|
type: "operator",
|
|
446
|
-
operator:
|
|
447
|
-
parentId:
|
|
448
|
-
position:
|
|
725
|
+
operator: newOperator,
|
|
726
|
+
parentId: formulaId,
|
|
727
|
+
position: POSITION_INITIAL,
|
|
449
728
|
};
|
|
450
|
-
this.expressions.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
operator: "not",
|
|
463
|
-
parentId: target.parentId,
|
|
464
|
-
position: target.position,
|
|
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,
|
|
465
741
|
};
|
|
466
|
-
this.expressions.insertExpression(notExpr, expressionId);
|
|
467
|
-
notExprId = notExpr.id;
|
|
468
742
|
}
|
|
469
|
-
this.syncRootExpressionId();
|
|
470
|
-
this.markDirty();
|
|
471
|
-
const changes = this.flushAndBuildChangeset(collector);
|
|
472
|
-
this.syncExpressionIndex(changes);
|
|
473
|
-
this.onMutate?.();
|
|
474
|
-
return {
|
|
475
|
-
result: this.expressions.getExpression(notExprId),
|
|
476
|
-
changes,
|
|
477
|
-
};
|
|
478
743
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
744
|
+
finally {
|
|
745
|
+
this.expressions.setCollector(null);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
483
748
|
}
|
|
484
749
|
getExpression(id) {
|
|
485
750
|
return this.expressions.getExpression(id);
|
|
@@ -492,20 +757,24 @@ export class PremiseEngine {
|
|
|
492
757
|
return { ...extras };
|
|
493
758
|
}
|
|
494
759
|
setExtras(extras) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
});
|
|
509
778
|
}
|
|
510
779
|
getRootExpressionId() {
|
|
511
780
|
return this.rootExpressionId;
|
|
@@ -682,7 +951,9 @@ export class PremiseEngine {
|
|
|
682
951
|
let value;
|
|
683
952
|
if (options?.resolver) {
|
|
684
953
|
const variable = this.variables.getVariable(expression.variableId);
|
|
685
|
-
if (variable &&
|
|
954
|
+
if (variable &&
|
|
955
|
+
isPremiseBound(variable) &&
|
|
956
|
+
!isExternallyBound(variable, this.argument.id)) {
|
|
686
957
|
value = options.resolver(expression.variableId);
|
|
687
958
|
}
|
|
688
959
|
else {
|
|
@@ -863,6 +1134,71 @@ export class PremiseEngine {
|
|
|
863
1134
|
: computeHash(this.cachedMetaChecksum + this.cachedDescendantChecksum);
|
|
864
1135
|
this.checksumDirty = false;
|
|
865
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
|
+
}
|
|
866
1202
|
// -------------------------------------------------------------------------
|
|
867
1203
|
// Private helpers
|
|
868
1204
|
// -------------------------------------------------------------------------
|
|
@@ -1017,6 +1353,8 @@ export class PremiseEngine {
|
|
|
1017
1353
|
* at mutation time by the ChangeCollector).
|
|
1018
1354
|
*/
|
|
1019
1355
|
flushAndBuildChangeset(collector) {
|
|
1356
|
+
// Snapshot premise combinedChecksum before flush
|
|
1357
|
+
const premiseCombinedBefore = this.cachedCombinedChecksum ?? null;
|
|
1020
1358
|
this.expressions.flushExpressionChecksums();
|
|
1021
1359
|
const changes = collector.toChangeset();
|
|
1022
1360
|
if (changes.expressions) {
|
|
@@ -1029,6 +1367,12 @@ export class PremiseEngine {
|
|
|
1029
1367
|
return current ? { ...current } : expr;
|
|
1030
1368
|
});
|
|
1031
1369
|
}
|
|
1370
|
+
// Recompute premise checksum and include if changed
|
|
1371
|
+
this.flushChecksums();
|
|
1372
|
+
if (this.cachedCombinedChecksum !== premiseCombinedBefore) {
|
|
1373
|
+
changes.premises ??= { added: [], modified: [], removed: [] };
|
|
1374
|
+
changes.premises.modified.push(this.toPremiseData());
|
|
1375
|
+
}
|
|
1032
1376
|
return changes;
|
|
1033
1377
|
}
|
|
1034
1378
|
syncExpressionIndex(changes) {
|