@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.
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 +28 -0
  28. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  29. package/dist/lib/core/expression-manager.js +458 -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 +53 -1
  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 +12 -1
  45. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  46. package/dist/lib/core/premise-engine.js +726 -382
  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,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
- deleteExpressionsUsingVariable(variableId) {
48
- const expressionIds = this.expressionsByVariableId.get(variableId);
49
- if (expressionIds.size === 0) {
50
- 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
+ }
51
68
  }
52
- const collector = new ChangeCollector();
53
- // Suppress onMutate during the loop to avoid redundant notifications
54
- const savedOnMutate = this.onMutate;
55
- this.onMutate = undefined;
56
- try {
57
- // Copy the set since removeExpression mutates expressionsByVariableId
58
- const removed = [];
59
- for (const exprId of [...expressionIds]) {
60
- // The expression may already have been removed as part of a
61
- // prior subtree deletion or operator collapse in this loop.
62
- if (!this.expressions.getExpression(exprId))
63
- continue;
64
- const { result, changes } = this.removeExpression(exprId, true);
65
- if (result)
66
- removed.push(result);
67
- if (changes.expressions) {
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
- // Expressions in the collector already have checksums attached
74
- // (from ExpressionManager which stores expressions with checksums).
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
- addExpression(expression) {
93
- this.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
94
- if (expression.type === "variable" &&
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
- if (expression.type === "variable" && this.circularityCheck) {
99
- if (this.circularityCheck(expression.variableId, this.premise.id)) {
100
- 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);
101
105
  }
106
+ return result;
102
107
  }
103
- if (expression.parentId === null) {
104
- if (this.rootExpressionId !== undefined) {
105
- throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
108
+ catch (e) {
109
+ if (!(e instanceof InvariantViolationError)) {
110
+ this.restoreFromPremiseSnapshot(snap);
106
111
  }
112
+ throw e;
107
113
  }
108
- else {
109
- if (!this.expressions.getExpression(expression.parentId)) {
110
- throw new Error(`Parent expression "${expression.parentId}" does not exist in this premise.`);
111
- }
114
+ finally {
115
+ this.insideValidation = false;
112
116
  }
113
- const collector = new ChangeCollector();
114
- this.expressions.setCollector(collector);
115
- try {
116
- // Delegate structural validation (operator type checks, position
117
- // uniqueness, child limits) to ExpressionManager.
118
- 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
+ }
119
177
  if (expression.parentId === null) {
120
- this.rootExpressionId = expression.id;
178
+ if (this.rootExpressionId !== undefined) {
179
+ throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
180
+ }
121
181
  }
122
- if (expression.type === "variable") {
123
- this.expressionsByVariableId
124
- .get(expression.variableId)
125
- .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
+ }
126
186
  }
127
- this.markDirty();
128
- const changes = this.flushAndBuildChangeset(collector);
129
- this.syncExpressionIndex(changes);
130
- this.onMutate?.();
131
- return {
132
- result: this.expressions.getExpression(expression.id),
133
- changes,
134
- };
135
- }
136
- finally {
137
- this.expressions.setCollector(null);
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.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
142
- if (expression.type === "variable" &&
143
- !this.variables.hasVariable(expression.variableId)) {
144
- throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
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
- else {
157
- if (!this.expressions.getExpression(parentId)) {
158
- 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
+ }
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.syncRootExpressionId();
228
+ if (this.rootExpressionId !== undefined) {
229
+ throw new Error(`Premise "${this.premise.id}" already has a root expression.`);
230
+ }
167
231
  }
168
- if (expression.type === "variable") {
169
- this.expressionsByVariableId
170
- .get(expression.variableId)
171
- .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
+ }
172
236
  }
173
- this.markDirty();
174
- const changes = this.flushAndBuildChangeset(collector);
175
- this.syncExpressionIndex(changes);
176
- this.onMutate?.();
177
- return {
178
- result: this.expressions.getExpression(expression.id),
179
- changes,
180
- };
181
- }
182
- finally {
183
- this.expressions.setCollector(null);
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.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
188
- if (expression.type === "variable" &&
189
- !this.variables.hasVariable(expression.variableId)) {
190
- throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
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
- if (!this.expressions.getExpression(siblingId)) {
198
- throw new Error(`Expression "${siblingId}" not found in this premise.`);
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.markDirty();
210
- const changes = this.flushAndBuildChangeset(collector);
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
- const collector = new ChangeCollector();
233
- this.expressions.setCollector(collector);
234
- try {
235
- const oldVariableId = existing.type === "variable" ? existing.variableId : undefined;
236
- const updated = this.expressions.updateExpression(expressionId, updates);
237
- if (updates.variableId !== undefined &&
238
- oldVariableId !== undefined &&
239
- oldVariableId !== updates.variableId) {
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
- this.syncExpressionIndex(changeset);
253
- return {
254
- result: updated,
255
- changes: changeset,
256
- };
257
- }
258
- finally {
259
- this.expressions.setCollector(null);
260
- }
296
+ finally {
297
+ this.expressions.setCollector(null);
298
+ }
299
+ });
261
300
  }
262
- removeExpression(expressionId, deleteSubtree) {
263
- // Snapshot the expression before removal (for result).
264
- const snapshot = this.expressions.getExpression(expressionId);
265
- const collector = new ChangeCollector();
266
- this.expressions.setCollector(collector);
267
- try {
268
- if (!snapshot) {
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: undefined,
271
- changes: collector.toChangeset(),
336
+ result: updated,
337
+ changes: changeset,
272
338
  };
273
339
  }
274
- if (deleteSubtree) {
275
- // Snapshot the subtree before deletion so we can clean up
276
- // expressionsByVariableId for cascade-deleted descendants — they are
277
- // not individually surfaced by ExpressionManager.removeExpression.
278
- const subtree = this.collectSubtree(expressionId);
279
- this.expressions.removeExpression(expressionId, true);
280
- for (const expr of subtree) {
281
- if (expr.type === "variable") {
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(expr.variableId)
284
- ?.delete(expr.id);
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
- else {
289
- // Only clean up expressionsByVariableId for the removed
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
- this.syncRootExpressionId();
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.assertBelongsToArgument(expression.argumentId, expression.argumentVersion);
314
- if (expression.type === "variable" &&
315
- !this.variables.hasVariable(expression.variableId)) {
316
- throw new Error(`Variable expression "${expression.id}" references non-existent variable "${expression.variableId}".`);
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
- const collector = new ChangeCollector();
324
- this.expressions.setCollector(collector);
325
- try {
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
- this.syncRootExpressionId();
333
- this.markDirty();
334
- const changes = this.flushAndBuildChangeset(collector);
335
- this.syncExpressionIndex(changes);
336
- this.onMutate?.();
337
- return {
338
- result: this.expressions.getExpression(expression.id),
339
- changes,
340
- };
341
- }
342
- finally {
343
- this.expressions.setCollector(null);
344
- }
345
- }
346
- wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
347
- this.assertBelongsToArgument(operator.argumentId, operator.argumentVersion);
348
- this.assertBelongsToArgument(newSibling.argumentId, newSibling.argumentVersion);
349
- if (newSibling.type === "variable" &&
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
- const collector = new ChangeCollector();
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
- this.syncRootExpressionId();
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
- toggleNegation(expressionId, extraFields) {
382
- const target = this.expressions.getExpression(expressionId);
383
- if (!target) {
384
- throw new Error(`Expression "${expressionId}" not found in this premise.`);
385
- }
386
- this.assertBelongsToArgument(target.argumentId, target.argumentVersion);
387
- const collector = new ChangeCollector();
388
- this.expressions.setCollector(collector);
389
- try {
390
- const parent = target.parentId
391
- ? this.expressions.getExpression(target.parentId)
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
- else {
410
- // Remove the NOT operator, promoting target into its slot
411
- this.expressions.removeExpression(parent.id, false);
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 { result: null, changes };
460
+ return {
461
+ result: this.expressions.getExpression(operator.id),
462
+ changes,
463
+ };
419
464
  }
420
- else {
421
- // When the target is a non-not operator, insert a formula
422
- // buffer between the new not and the target so the tree
423
- // satisfies the operator nesting restriction.
424
- const needsFormula = target.type === "operator" && target.operator !== "not";
425
- let notExprId;
426
- if (needsFormula) {
427
- // Build not → formula → target
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: randomUUID(),
708
+ id: formulaId,
431
709
  argumentId: target.argumentId,
432
710
  argumentVersion: target.argumentVersion,
433
711
  premiseId: target.premiseId,
434
712
  type: "formula",
435
- parentId: target.parentId,
436
- position: target.position,
713
+ parentId: expressionId,
714
+ position: formulaPosition,
437
715
  };
438
- this.expressions.insertExpression(formulaExpr, expressionId);
439
- const notExpr = {
716
+ this.expressions.addExpression(formulaExpr);
717
+ // Add the new sub-operator under the formula
718
+ const newOpExpr = {
440
719
  ...extraFields,
441
- id: randomUUID(),
720
+ id: newOpId,
442
721
  argumentId: target.argumentId,
443
722
  argumentVersion: target.argumentVersion,
444
723
  premiseId: target.premiseId,
445
724
  type: "operator",
446
- operator: "not",
447
- parentId: target.parentId,
448
- position: target.position,
725
+ operator: newOperator,
726
+ parentId: formulaId,
727
+ position: POSITION_INITIAL,
449
728
  };
450
- this.expressions.insertExpression(notExpr, formulaExpr.id);
451
- notExprId = notExpr.id;
452
- }
453
- else {
454
- // Wrap target with a new NOT operator
455
- const notExpr = {
456
- ...extraFields,
457
- id: randomUUID(),
458
- argumentId: target.argumentId,
459
- argumentVersion: target.argumentVersion,
460
- premiseId: target.premiseId,
461
- type: "operator",
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
- finally {
481
- this.expressions.setCollector(null);
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
- // Strip old extras and replace with new ones
496
- const { id, argumentId, argumentVersion, checksum, descendantChecksum, combinedChecksum, } = this.premise;
497
- this.premise = {
498
- ...extras,
499
- id,
500
- argumentId,
501
- argumentVersion,
502
- ...(checksum !== undefined ? { checksum } : {}),
503
- ...(descendantChecksum !== undefined ? { descendantChecksum } : {}),
504
- ...(combinedChecksum !== undefined ? { combinedChecksum } : {}),
505
- };
506
- this.markDirty();
507
- this.onMutate?.();
508
- 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
+ });
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 && isPremiseBound(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) {