@polintpro/proposit-core 0.6.6 → 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.
Files changed (81) hide show
  1. package/README.md +194 -0
  2. package/dist/extensions/basics/schemata.d.ts +5 -0
  3. package/dist/extensions/basics/schemata.d.ts.map +1 -1
  4. package/dist/lib/consts.d.ts.map +1 -1
  5. package/dist/lib/consts.js +21 -2
  6. package/dist/lib/consts.js.map +1 -1
  7. package/dist/lib/core/argument-engine.d.ts +51 -2
  8. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  9. package/dist/lib/core/argument-engine.js +764 -227
  10. package/dist/lib/core/argument-engine.js.map +1 -1
  11. package/dist/lib/core/change-collector.d.ts +1 -0
  12. package/dist/lib/core/change-collector.d.ts.map +1 -1
  13. package/dist/lib/core/change-collector.js +3 -0
  14. package/dist/lib/core/change-collector.js.map +1 -1
  15. package/dist/lib/core/claim-library.d.ts +4 -0
  16. package/dist/lib/core/claim-library.d.ts.map +1 -1
  17. package/dist/lib/core/claim-library.js +126 -59
  18. package/dist/lib/core/claim-library.js.map +1 -1
  19. package/dist/lib/core/claim-source-library.d.ts +4 -0
  20. package/dist/lib/core/claim-source-library.d.ts.map +1 -1
  21. package/dist/lib/core/claim-source-library.js +114 -38
  22. package/dist/lib/core/claim-source-library.js.map +1 -1
  23. package/dist/lib/core/diff.d.ts +10 -0
  24. package/dist/lib/core/diff.d.ts.map +1 -1
  25. package/dist/lib/core/diff.js +114 -21
  26. package/dist/lib/core/diff.js.map +1 -1
  27. package/dist/lib/core/expression-manager.d.ts +11 -0
  28. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  29. package/dist/lib/core/expression-manager.js +379 -20
  30. package/dist/lib/core/expression-manager.js.map +1 -1
  31. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +9 -2
  32. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  33. package/dist/lib/core/interfaces/library.interfaces.d.ts +19 -0
  34. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  35. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +22 -0
  36. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  37. package/dist/lib/core/invariant-violation-error.d.ts +6 -0
  38. package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
  39. package/dist/lib/core/invariant-violation-error.js +12 -0
  40. package/dist/lib/core/invariant-violation-error.js.map +1 -0
  41. package/dist/lib/core/parser/formula.d.ts.map +1 -1
  42. package/dist/lib/core/parser/formula.js +2 -2
  43. package/dist/lib/core/parser/formula.js.map +1 -1
  44. package/dist/lib/core/premise-engine.d.ts +10 -0
  45. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  46. package/dist/lib/core/premise-engine.js +699 -536
  47. package/dist/lib/core/premise-engine.js.map +1 -1
  48. package/dist/lib/core/source-library.d.ts +4 -0
  49. package/dist/lib/core/source-library.d.ts.map +1 -1
  50. package/dist/lib/core/source-library.js +126 -59
  51. package/dist/lib/core/source-library.js.map +1 -1
  52. package/dist/lib/core/variable-manager.d.ts +7 -0
  53. package/dist/lib/core/variable-manager.d.ts.map +1 -1
  54. package/dist/lib/core/variable-manager.js +65 -1
  55. package/dist/lib/core/variable-manager.js.map +1 -1
  56. package/dist/lib/index.d.ts +4 -1
  57. package/dist/lib/index.d.ts.map +1 -1
  58. package/dist/lib/index.js +4 -1
  59. package/dist/lib/index.js.map +1 -1
  60. package/dist/lib/schemata/argument.d.ts +2 -0
  61. package/dist/lib/schemata/argument.d.ts.map +1 -1
  62. package/dist/lib/schemata/argument.js +6 -0
  63. package/dist/lib/schemata/argument.js.map +1 -1
  64. package/dist/lib/schemata/propositional.d.ts +41 -0
  65. package/dist/lib/schemata/propositional.d.ts.map +1 -1
  66. package/dist/lib/schemata/propositional.js +34 -0
  67. package/dist/lib/schemata/propositional.js.map +1 -1
  68. package/dist/lib/types/diff.d.ts +6 -0
  69. package/dist/lib/types/diff.d.ts.map +1 -1
  70. package/dist/lib/types/fork.d.ts +32 -0
  71. package/dist/lib/types/fork.d.ts.map +1 -0
  72. package/dist/lib/types/fork.js +2 -0
  73. package/dist/lib/types/fork.js.map +1 -0
  74. package/dist/lib/types/grammar.d.ts +5 -4
  75. package/dist/lib/types/grammar.d.ts.map +1 -1
  76. package/dist/lib/types/grammar.js.map +1 -1
  77. package/dist/lib/types/validation.d.ts +46 -0
  78. package/dist/lib/types/validation.d.ts.map +1 -0
  79. package/dist/lib/types/validation.js +41 -0
  80. package/dist/lib/types/validation.js.map +1 -0
  81. 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
- deleteExpressionsUsingVariable(variableId) {
49
- const expressionIds = this.expressionsByVariableId.get(variableId);
50
- if (expressionIds.size === 0) {
51
- return { result: [], changes: {} };
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
- const collector = new ChangeCollector();
54
- // Suppress onMutate during the loop to avoid redundant notifications
55
- const savedOnMutate = this.onMutate;
56
- this.onMutate = undefined;
57
- try {
58
- // Copy the set since removeExpression mutates expressionsByVariableId
59
- const removed = [];
60
- for (const exprId of [...expressionIds]) {
61
- // The expression may already have been removed as part of a
62
- // prior subtree deletion or operator collapse in this loop.
63
- if (!this.expressions.getExpression(exprId))
64
- continue;
65
- const { result, changes } = this.removeExpression(exprId, true);
66
- if (result)
67
- removed.push(result);
68
- if (changes.expressions) {
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
- // Expressions in the collector already have checksums attached
75
- // (from ExpressionManager which stores expressions with checksums).
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
- addExpression(expression) {
94
- this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
95
- if (expression.type === "variable" &&
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
- if (expression.type === "variable" && this.circularityCheck) {
100
- if (this.circularityCheck(expression.variableId, this.premise.id)) {
101
- throw new Error(`Circular binding: variable "${expression.variableId}" is bound to this premise (directly or transitively)`);
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
- if (expression.parentId === null) {
105
- if (this.rootExpressionId !== undefined) {
106
- throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
108
+ catch (e) {
109
+ if (!(e instanceof InvariantViolationError)) {
110
+ this.restoreFromPremiseSnapshot(snap);
107
111
  }
112
+ throw e;
108
113
  }
109
- else {
110
- if (!this.expressions.getExpression(expression.parentId)) {
111
- throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
112
- }
114
+ finally {
115
+ this.insideValidation = false;
113
116
  }
114
- const collector = new ChangeCollector();
115
- this.expressions.setCollector(collector);
116
- try {
117
- // Delegate structural validation (operator type checks, position
118
- // uniqueness, child limits) to ExpressionManager.
119
- this.expressions.addExpression(expression);
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 = expression.id;
178
+ if (this.rootExpressionId !== undefined) {
179
+ throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
180
+ }
122
181
  }
123
- if (expression.type === "variable") {
124
- this.expressionsByVariableId
125
- .get(expression.variableId)
126
- .add(expression.id);
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
- this.markDirty();
129
- const changes = this.flushAndBuildChangeset(collector);
130
- this.syncExpressionIndex(changes);
131
- this.onMutate?.();
132
- return {
133
- result: this.expressions.getExpression(expression.id),
134
- changes,
135
- };
136
- }
137
- finally {
138
- this.expressions.setCollector(null);
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.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
143
- if (expression.type === "variable" &&
144
- !this.variables.hasVariable(expression.variableId)) {
145
- throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
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
- if (parentId === null) {
153
- if (this.rootExpressionId !== undefined) {
154
- throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
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.syncRootExpressionId();
228
+ if (this.rootExpressionId !== undefined) {
229
+ throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
230
+ }
168
231
  }
169
- if (expression.type === "variable") {
170
- this.expressionsByVariableId
171
- .get(expression.variableId)
172
- .add(expression.id);
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
- this.markDirty();
175
- const changes = this.flushAndBuildChangeset(collector);
176
- this.syncExpressionIndex(changes);
177
- this.onMutate?.();
178
- return {
179
- result: this.expressions.getExpression(expression.id),
180
- changes,
181
- };
182
- }
183
- finally {
184
- this.expressions.setCollector(null);
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.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
189
- if (expression.type === "variable" &&
190
- !this.variables.hasVariable(expression.variableId)) {
191
- throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
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
- if (!this.expressions.getExpression(siblingId)) {
199
- throw new Error(`Expression "${siblingId}" not found in this premise.`);
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.markDirty();
211
- const changes = this.flushAndBuildChangeset(collector);
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
- const collector = new ChangeCollector();
234
- this.expressions.setCollector(collector);
235
- try {
236
- const oldVariableId = existing.type === "variable" ? existing.variableId : undefined;
237
- const updated = this.expressions.updateExpression(expressionId, updates);
238
- if (updates.variableId !== undefined &&
239
- oldVariableId !== undefined &&
240
- oldVariableId !== updates.variableId) {
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: undefined,
272
- changes: collector.toChangeset(),
292
+ result: this.expressions.getExpression(expression.id),
293
+ changes,
273
294
  };
274
295
  }
275
- if (deleteSubtree) {
276
- // Snapshot the subtree before deletion so we can clean up
277
- // expressionsByVariableId for cascade-deleted descendants — they are
278
- // not individually surfaced by ExpressionManager.removeExpression.
279
- const subtree = this.collectSubtree(expressionId);
280
- this.expressions.removeExpression(expressionId, true);
281
- for (const expr of subtree) {
282
- if (expr.type === "variable") {
283
- this.expressionsByVariableId
284
- .get(expr.variableId)
285
- ?.delete(expr.id);
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
- else {
290
- // Only clean up expressionsByVariableId for the removed
291
- // expression itself — children survive promotion.
292
- if (snapshot.type === "variable") {
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(snapshot.variableId)
295
- ?.delete(snapshot.id);
323
+ .get(oldVariableId)
324
+ ?.delete(expressionId);
325
+ this.expressionsByVariableId
326
+ .get(updates.variableId)
327
+ .add(expressionId);
296
328
  }
297
- this.expressions.removeExpression(expressionId, false);
298
- }
299
- this.syncRootExpressionId();
300
- this.markDirty();
301
- const changes = this.flushAndBuildChangeset(collector);
302
- this.syncExpressionIndex(changes);
303
- this.onMutate?.();
304
- return {
305
- result: snapshot,
306
- changes,
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
- const collector = new ChangeCollector();
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
- this.syncRootExpressionId();
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
- wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
348
- this.assertBelongsToArgument(operator.argumentId, operator.argumentVersion);
349
- this.assertBelongsToArgument(newSibling.argumentId, newSibling.argumentVersion);
350
- if (newSibling.type === "variable" &&
351
- !this.variables.hasVariable(newSibling.variableId)) {
352
- throw new Error(`Variable expression "${newSibling.id}" references non-existent variable "${newSibling.variableId}".`);
353
- }
354
- if (newSibling.type === "variable" && this.circularityCheck) {
355
- if (this.circularityCheck(newSibling.variableId, this.premise.id)) {
356
- throw new Error(`Circular binding: variable "${newSibling.variableId}" is bound to this premise (directly or transitively)`);
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
- const collector = new ChangeCollector();
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
- this.syncRootExpressionId();
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
- toggleNegation(expressionId, extraFields) {
383
- const target = this.expressions.getExpression(expressionId);
384
- if (!target) {
385
- throw new Error(`Expression "${expressionId}" not found in this premise.`);
386
- }
387
- this.assertBelongsToArgument(target.argumentId, target.argumentVersion);
388
- const collector = new ChangeCollector();
389
- this.expressions.setCollector(collector);
390
- try {
391
- const parent = target.parentId
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
- else {
411
- // Remove the NOT operator, promoting target into its slot
412
- this.expressions.removeExpression(parent.id, false);
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 { result: null, changes };
423
+ return {
424
+ result: this.expressions.getExpression(expression.id),
425
+ changes,
426
+ };
420
427
  }
421
- else {
422
- // When the target is a non-not operator, insert a formula
423
- // buffer between the new not and the target so the tree
424
- // satisfies the operator nesting restriction.
425
- const needsFormula = target.type === "operator" && target.operator !== "not";
426
- let notExprId;
427
- if (needsFormula) {
428
- // Build not → formula → target
429
- const formulaExpr = {
430
- ...extraFields,
431
- id: randomUUID(),
432
- argumentId: target.argumentId,
433
- argumentVersion: target.argumentVersion,
434
- premiseId: target.premiseId,
435
- type: "formula",
436
- parentId: target.parentId,
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
- else {
455
- // Wrap target with a new NOT operator
456
- const notExpr = {
457
- ...extraFields,
458
- id: randomUUID(),
459
- argumentId: target.argumentId,
460
- argumentVersion: target.argumentVersion,
461
- premiseId: target.premiseId,
462
- type: "operator",
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(notExprId),
461
+ result: this.expressions.getExpression(operator.id),
477
462
  changes,
478
463
  };
479
464
  }
480
- }
481
- finally {
482
- this.expressions.setCollector(null);
483
- }
465
+ finally {
466
+ this.expressions.setCollector(null);
467
+ }
468
+ });
484
469
  }
485
- changeOperator(expressionId, newOperator, sourceChildId, targetChildId, extraFields) {
486
- const target = this.expressions.getExpression(expressionId);
487
- if (!target) {
488
- throw new Error(`Expression "${expressionId}" not found in this premise.`);
489
- }
490
- if (target.type !== "operator") {
491
- throw new Error(`Expression "${expressionId}" is not an operator expression (type: "${target.type}").`);
492
- }
493
- if (target.type === "operator" && target.operator === "not") {
494
- throw new Error(`Cannot change a "not" operator. Use toggleNegation instead.`);
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
- // Look through formula buffer: if parent is formula, check grandparent
512
- let mergeTarget;
513
- if (parent?.type === "formula" && parent.parentId) {
514
- const grandparent = this.expressions.getExpression(parent.parentId);
515
- if (grandparent?.type === "operator" &&
516
- grandparent.operator === newOperator) {
517
- mergeTarget = grandparent;
518
- }
519
- }
520
- else if (parent?.type === "operator" &&
521
- parent.operator === newOperator) {
522
- mergeTarget = parent;
523
- }
524
- if (mergeTarget) {
525
- // --- MERGE ---
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
- // Delete the dissolving operator (now has no children)
553
- this.expressions.deleteExpression(expressionId);
554
- // Delete the formula buffer if it existed (now has no children)
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
- // --- SIMPLE CHANGE ---
567
- this.expressions.changeOperatorType(expressionId, newOperator);
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(expressionId),
565
+ result: this.expressions.getExpression(notExprId),
575
566
  changes,
576
567
  };
577
568
  }
578
569
  }
579
- else {
580
- // --- SPLIT (>2 children) ---
581
- if (!sourceChildId || !targetChildId) {
582
- throw new Error(`Operator "${expressionId}" has ${childCount} children — sourceChildId and targetChildId are required for split.`);
583
- }
584
- // Validate source and target are children of the operator
585
- const sourceChild = this.expressions.getExpression(sourceChildId);
586
- const targetChild = this.expressions.getExpression(targetChildId);
587
- if (!sourceChild || sourceChild.parentId !== expressionId) {
588
- throw new Error(`Expression "${sourceChildId}" is not a child of operator "${expressionId}".`);
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
- if (!targetChild || targetChild.parentId !== expressionId) {
591
- throw new Error(`Expression "${targetChildId}" is not a child of operator "${expressionId}".`);
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
- finally {
654
- this.expressions.setCollector(null);
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
- // Strip old extras and replace with new ones
669
- const { id, argumentId, argumentVersion, checksum, descendantChecksum, combinedChecksum, } = this.premise;
670
- this.premise = {
671
- ...extras,
672
- id,
673
- argumentId,
674
- argumentVersion,
675
- ...(checksum !== undefined ? { checksum } : {}),
676
- ...(descendantChecksum !== undefined ? { descendantChecksum } : {}),
677
- ...(combinedChecksum !== undefined ? { combinedChecksum } : {}),
678
- };
679
- this.markDirty();
680
- this.onMutate?.();
681
- return { result: this.getExtras(), changes: {} };
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 && isPremiseBound(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
  // -------------------------------------------------------------------------