@proposit/proposit-core 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/conversation/turn.js +1 -1
- package/dist/lib/conversation/turn.js.map +1 -1
- package/dist/lib/core/expression-manager-checks.d.ts +73 -0
- package/dist/lib/core/expression-manager-checks.d.ts.map +1 -0
- package/dist/lib/core/expression-manager-checks.js +355 -0
- package/dist/lib/core/expression-manager-checks.js.map +1 -0
- package/dist/lib/core/expression-manager-dirty-set.d.ts +21 -0
- package/dist/lib/core/expression-manager-dirty-set.d.ts.map +1 -0
- package/dist/lib/core/expression-manager-dirty-set.js +93 -0
- package/dist/lib/core/expression-manager-dirty-set.js.map +1 -0
- package/dist/lib/core/expression-manager-invariants.d.ts +14 -0
- package/dist/lib/core/expression-manager-invariants.d.ts.map +1 -0
- package/dist/lib/core/expression-manager-invariants.js +171 -0
- package/dist/lib/core/expression-manager-invariants.js.map +1 -0
- package/dist/lib/core/expression-manager.d.ts +2 -3
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +18 -523
- package/dist/lib/core/expression-manager.js.map +1 -1
- package/dist/lib/pipelines/index.d.ts +4 -2
- package/dist/lib/pipelines/index.d.ts.map +1 -1
- package/dist/lib/pipelines/index.js +2 -1
- package/dist/lib/pipelines/index.js.map +1 -1
- package/dist/lib/pipelines/llm-stage-helpers.d.ts +126 -0
- package/dist/lib/pipelines/llm-stage-helpers.d.ts.map +1 -0
- package/dist/lib/pipelines/llm-stage-helpers.js +567 -0
- package/dist/lib/pipelines/llm-stage-helpers.js.map +1 -0
- package/dist/lib/pipelines/scheduler.d.ts +62 -0
- package/dist/lib/pipelines/scheduler.d.ts.map +1 -0
- package/dist/lib/pipelines/{execute.js → scheduler.js} +8 -325
- package/dist/lib/pipelines/scheduler.js.map +1 -0
- package/dist/lib/pipelines/{execute.d.ts → single-stage.d.ts} +2 -22
- package/dist/lib/pipelines/{execute.d.ts.map → single-stage.d.ts.map} +1 -1
- package/dist/lib/pipelines/single-stage.js +333 -0
- package/dist/lib/pipelines/single-stage.js.map +1 -0
- package/dist/lib/pipelines/stage-helpers.d.ts +4 -123
- package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
- package/dist/lib/pipelines/stage-helpers.js +28 -567
- package/dist/lib/pipelines/stage-helpers.js.map +1 -1
- package/package.json +3 -3
- package/dist/lib/pipelines/execute.js.map +0 -1
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { CorePropositionalExpressionSchema } from "../schemata/index.js";
|
|
2
1
|
import { getOrCreate } from "../utils/collections.js";
|
|
3
2
|
import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
|
|
4
3
|
import { defaultGenerateId } from "./argument-engine.js";
|
|
5
4
|
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
6
|
-
import { entityChecksum
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
and: "or",
|
|
11
|
-
or: "and",
|
|
12
|
-
implies: "iff",
|
|
13
|
-
iff: "implies",
|
|
14
|
-
};
|
|
5
|
+
import { entityChecksum } from "./checksum.js";
|
|
6
|
+
import { markExpressionDirty, flushExpressionChecksums, pruneDeletedFromDirtySet, } from "./expression-manager-dirty-set.js";
|
|
7
|
+
import { validateExpressionManagerInvariants } from "./expression-manager-invariants.js";
|
|
8
|
+
import { validateInsertExpression, validateWrapExpression, validateRepositionSiblings, validateUpdateExpression, validateRemoveAndPromote, validateAddExpressionRelative, } from "./expression-manager-checks.js";
|
|
15
9
|
/**
|
|
16
10
|
* Low-level manager for a flat-stored expression tree.
|
|
17
11
|
*
|
|
@@ -143,14 +137,7 @@ export class ExpressionManager {
|
|
|
143
137
|
* already in the dirty set (since its ancestors are already marked).
|
|
144
138
|
*/
|
|
145
139
|
markExpressionDirty(exprId) {
|
|
146
|
-
|
|
147
|
-
while (current !== null) {
|
|
148
|
-
if (this.dirtyExpressionIds.has(current))
|
|
149
|
-
break; // ancestors already dirty
|
|
150
|
-
this.dirtyExpressionIds.add(current);
|
|
151
|
-
const expr = this.expressions.get(current);
|
|
152
|
-
current = expr ? expr.parentId : null;
|
|
153
|
-
}
|
|
140
|
+
markExpressionDirty(exprId, this.dirtyExpressionIds, this.expressions);
|
|
154
141
|
}
|
|
155
142
|
/**
|
|
156
143
|
* Recomputes `descendantChecksum` and `combinedChecksum` for all dirty
|
|
@@ -158,74 +145,14 @@ export class ExpressionManager {
|
|
|
158
145
|
* are up-to-date before their parents are computed.
|
|
159
146
|
*/
|
|
160
147
|
flushExpressionChecksums() {
|
|
161
|
-
|
|
162
|
-
return;
|
|
163
|
-
// Sort dirty expressions by depth (deepest first) for bottom-up processing
|
|
164
|
-
const dirtyIds = [...this.dirtyExpressionIds];
|
|
165
|
-
const depthOf = (id) => {
|
|
166
|
-
let depth = 0;
|
|
167
|
-
let current = this.expressions.get(id);
|
|
168
|
-
while (current && current.parentId !== null) {
|
|
169
|
-
depth++;
|
|
170
|
-
current = this.expressions.get(current.parentId);
|
|
171
|
-
}
|
|
172
|
-
return depth;
|
|
173
|
-
};
|
|
174
|
-
dirtyIds.sort((a, b) => depthOf(b) - depthOf(a));
|
|
175
|
-
const fields = this.config?.checksumConfig?.expressionFields ??
|
|
176
|
-
DEFAULT_CHECKSUM_CONFIG.expressionFields;
|
|
177
|
-
for (const id of dirtyIds) {
|
|
178
|
-
const expr = this.expressions.get(id);
|
|
179
|
-
if (!expr)
|
|
180
|
-
continue;
|
|
181
|
-
const oldChecksum = expr.checksum;
|
|
182
|
-
const oldDescendantChecksum = expr.descendantChecksum;
|
|
183
|
-
const oldCombinedChecksum = expr.combinedChecksum;
|
|
184
|
-
const metaChecksum = entityChecksum(expr, fields);
|
|
185
|
-
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
186
|
-
let descendantChecksum = null;
|
|
187
|
-
if (childIds && childIds.size > 0) {
|
|
188
|
-
const childMap = {};
|
|
189
|
-
for (const childId of childIds) {
|
|
190
|
-
const child = this.expressions.get(childId);
|
|
191
|
-
if (child) {
|
|
192
|
-
childMap[childId] = child.combinedChecksum;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
descendantChecksum = computeHash(canonicalSerialize(childMap));
|
|
196
|
-
}
|
|
197
|
-
const combinedChecksum = descendantChecksum === null
|
|
198
|
-
? metaChecksum
|
|
199
|
-
: computeHash(metaChecksum + descendantChecksum);
|
|
200
|
-
this.expressions.set(id, {
|
|
201
|
-
...expr,
|
|
202
|
-
checksum: metaChecksum,
|
|
203
|
-
descendantChecksum,
|
|
204
|
-
combinedChecksum,
|
|
205
|
-
});
|
|
206
|
-
if (this.collector &&
|
|
207
|
-
!this.collector.isExpressionAdded(expr.id) &&
|
|
208
|
-
(metaChecksum !== oldChecksum ||
|
|
209
|
-
descendantChecksum !== oldDescendantChecksum ||
|
|
210
|
-
combinedChecksum !== oldCombinedChecksum)) {
|
|
211
|
-
this.collector.modifiedExpression({
|
|
212
|
-
...expr,
|
|
213
|
-
checksum: metaChecksum,
|
|
214
|
-
descendantChecksum,
|
|
215
|
-
combinedChecksum,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
this.dirtyExpressionIds.clear();
|
|
148
|
+
flushExpressionChecksums(this.dirtyExpressionIds, this.expressions, this.childExpressionIdsByParentId, this.config, this.collector);
|
|
220
149
|
}
|
|
221
150
|
/**
|
|
222
151
|
* Removes deleted expression IDs from the dirty set so that flush
|
|
223
152
|
* doesn't attempt to process expressions that no longer exist.
|
|
224
153
|
*/
|
|
225
154
|
pruneDeletedFromDirtySet(deletedIds) {
|
|
226
|
-
|
|
227
|
-
this.dirtyExpressionIds.delete(id);
|
|
228
|
-
}
|
|
155
|
+
pruneDeletedFromDirtySet(deletedIds, this.dirtyExpressionIds);
|
|
229
156
|
}
|
|
230
157
|
/** Returns all expressions sorted by ID for deterministic output. */
|
|
231
158
|
toArray() {
|
|
@@ -328,10 +255,7 @@ export class ExpressionManager {
|
|
|
328
255
|
* @throws If the sibling does not exist.
|
|
329
256
|
*/
|
|
330
257
|
addExpressionRelative(siblingId, relativePosition, expression) {
|
|
331
|
-
const sibling = this.expressions
|
|
332
|
-
if (!sibling) {
|
|
333
|
-
throw new Error(`Expression "${siblingId}" not found.`);
|
|
334
|
-
}
|
|
258
|
+
const sibling = validateAddExpressionRelative(siblingId, this.expressions);
|
|
335
259
|
const children = this.getChildExpressions(sibling.parentId);
|
|
336
260
|
const siblingIndex = children.findIndex((c) => c.id === siblingId);
|
|
337
261
|
let position;
|
|
@@ -402,56 +326,13 @@ export class ExpressionManager {
|
|
|
402
326
|
if (!expression) {
|
|
403
327
|
throw new Error(`Expression "${expressionId}" not found.`);
|
|
404
328
|
}
|
|
405
|
-
|
|
406
|
-
const FORBIDDEN_KEYS = [
|
|
407
|
-
"id",
|
|
408
|
-
"argumentId",
|
|
409
|
-
"argumentVersion",
|
|
410
|
-
"premiseId",
|
|
411
|
-
"checksum",
|
|
412
|
-
"parentId",
|
|
413
|
-
"type",
|
|
414
|
-
];
|
|
415
|
-
for (const key of FORBIDDEN_KEYS) {
|
|
416
|
-
if (key in updates) {
|
|
417
|
-
throw new Error(`Field "${key}" is forbidden in expression updates.`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
329
|
+
validateUpdateExpression(expressionId, expression, updates, this.childPositionsByParentId);
|
|
420
330
|
// If no actual mutable fields are set, return the expression as-is.
|
|
421
331
|
if (updates.position === undefined &&
|
|
422
332
|
updates.variableId === undefined &&
|
|
423
333
|
updates.operator === undefined) {
|
|
424
334
|
return expression;
|
|
425
335
|
}
|
|
426
|
-
// Validate operator change.
|
|
427
|
-
if (updates.operator !== undefined) {
|
|
428
|
-
if (expression.type !== "operator") {
|
|
429
|
-
throw new Error(`Expression "${expressionId}" is not an operator expression; cannot update operator.`);
|
|
430
|
-
}
|
|
431
|
-
const permitted = PERMITTED_OPERATOR_SWAPS[expression.operator];
|
|
432
|
-
if (permitted !== updates.operator) {
|
|
433
|
-
throw new Error(`Changing operator from "${expression.operator}" to "${updates.operator}" is not a permitted operator change. Permitted: and↔or, implies↔iff.`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Validate variableId change.
|
|
437
|
-
if (updates.variableId !== undefined) {
|
|
438
|
-
if (expression.type !== "variable") {
|
|
439
|
-
throw new Error(`Expression "${expressionId}" is not a variable expression; cannot update variableId.`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Validate position change.
|
|
443
|
-
if (updates.position !== undefined) {
|
|
444
|
-
const positionSet = this.childPositionsByParentId.get(expression.parentId);
|
|
445
|
-
if (positionSet) {
|
|
446
|
-
positionSet.delete(expression.position);
|
|
447
|
-
if (positionSet.has(updates.position)) {
|
|
448
|
-
// Restore old position before throwing.
|
|
449
|
-
positionSet.add(expression.position);
|
|
450
|
-
throw new Error(`Position ${updates.position} is already used under parent "${expression.parentId}".`);
|
|
451
|
-
}
|
|
452
|
-
positionSet.add(updates.position);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
336
|
// Build an updated copy and replace in the map.
|
|
456
337
|
const updated = this.attachChecksum({
|
|
457
338
|
...expression,
|
|
@@ -552,10 +433,8 @@ export class ExpressionManager {
|
|
|
552
433
|
}
|
|
553
434
|
removeAndPromote(expressionId, target) {
|
|
554
435
|
const children = this.getChildExpressions(expressionId);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
if (children.length === 0) {
|
|
436
|
+
const child = validateRemoveAndPromote(expressionId, target, children);
|
|
437
|
+
if (child === undefined) {
|
|
559
438
|
// Leaf removal.
|
|
560
439
|
const parentId = target.parentId;
|
|
561
440
|
this.collector?.removedExpression({
|
|
@@ -572,18 +451,6 @@ export class ExpressionManager {
|
|
|
572
451
|
return target;
|
|
573
452
|
}
|
|
574
453
|
// Exactly 1 child — promote it into the target's slot.
|
|
575
|
-
const child = children[0];
|
|
576
|
-
// The P-1 promote-on-remove enforcement throw lived here under
|
|
577
|
-
// `grammarConfig.enforceFormulaBetweenOperators`. AN-1 (post-mutation
|
|
578
|
-
// hook in assistive mode) now inserts the buffer if the promotion
|
|
579
|
-
// produced a non-not operator under operator; permissive mode leaves
|
|
580
|
-
// the un-buffered state and `validate('presentable')` flags it.
|
|
581
|
-
// Validate: root-only operators cannot be promoted into a non-root position.
|
|
582
|
-
if (child.type === "operator" &&
|
|
583
|
-
(child.operator === "implies" || child.operator === "iff") &&
|
|
584
|
-
target.parentId !== null) {
|
|
585
|
-
throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
|
|
586
|
-
}
|
|
587
454
|
// Promote child into the target's slot.
|
|
588
455
|
const promoted = this.attachChecksum({
|
|
589
456
|
...child,
|
|
@@ -636,84 +503,11 @@ export class ExpressionManager {
|
|
|
636
503
|
const children = this.getChildExpressions(parentId);
|
|
637
504
|
if (children.length === 0)
|
|
638
505
|
return [];
|
|
639
|
-
const
|
|
640
|
-
const leftIdx = positions.indexOf(leftPos);
|
|
641
|
-
const rightIdx = positions.indexOf(rightPos);
|
|
642
|
-
// Scan left from leftIdx: expand while consecutive gaps <= 1.
|
|
643
|
-
let scanLeft;
|
|
644
|
-
let leftBound;
|
|
645
|
-
let leftCount;
|
|
646
|
-
if (leftIdx === -1) {
|
|
647
|
-
// leftPos is a boundary (positionConfig.min), not a real node.
|
|
648
|
-
scanLeft = 0;
|
|
649
|
-
leftBound = leftPos;
|
|
650
|
-
leftCount = 0;
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
scanLeft = leftIdx;
|
|
654
|
-
while (scanLeft > 0 &&
|
|
655
|
-
positions[scanLeft] - positions[scanLeft - 1] <= 1) {
|
|
656
|
-
scanLeft--;
|
|
657
|
-
}
|
|
658
|
-
leftBound =
|
|
659
|
-
scanLeft > 0 ? positions[scanLeft - 1] : this.positionConfig.min;
|
|
660
|
-
leftCount = leftIdx - scanLeft + 1;
|
|
661
|
-
}
|
|
662
|
-
// Scan right from rightIdx: expand while consecutive gaps <= 1.
|
|
663
|
-
let scanRight;
|
|
664
|
-
let rightBound;
|
|
665
|
-
let rightCount;
|
|
666
|
-
if (rightIdx === -1) {
|
|
667
|
-
// rightPos is a boundary (positionConfig.max), not a real node.
|
|
668
|
-
scanRight = positions.length - 1;
|
|
669
|
-
rightBound = rightPos;
|
|
670
|
-
rightCount = 0;
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
scanRight = rightIdx;
|
|
674
|
-
while (scanRight < positions.length - 1 &&
|
|
675
|
-
positions[scanRight + 1] - positions[scanRight] <= 1) {
|
|
676
|
-
scanRight++;
|
|
677
|
-
}
|
|
678
|
-
rightBound =
|
|
679
|
-
scanRight < positions.length - 1
|
|
680
|
-
? positions[scanRight + 1]
|
|
681
|
-
: this.positionConfig.max;
|
|
682
|
-
rightCount = scanRight - rightIdx + 1;
|
|
683
|
-
}
|
|
684
|
-
// Pick direction with fewer nodes. Tie-break: right.
|
|
685
|
-
let startIdx;
|
|
686
|
-
let endIdx;
|
|
687
|
-
let lowerBound;
|
|
688
|
-
let upperBound;
|
|
689
|
-
if (leftCount > 0 && leftCount < rightCount) {
|
|
690
|
-
startIdx = scanLeft;
|
|
691
|
-
endIdx = leftIdx;
|
|
692
|
-
lowerBound = leftBound;
|
|
693
|
-
upperBound = rightPos;
|
|
694
|
-
}
|
|
695
|
-
else if (rightCount > 0) {
|
|
696
|
-
startIdx = rightIdx;
|
|
697
|
-
endIdx = scanRight;
|
|
698
|
-
lowerBound = leftPos;
|
|
699
|
-
upperBound = rightBound;
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
// leftCount > 0, rightCount === 0: must pick left.
|
|
703
|
-
startIdx = scanLeft;
|
|
704
|
-
endIdx = leftIdx;
|
|
705
|
-
lowerBound = leftBound;
|
|
706
|
-
upperBound = rightPos;
|
|
707
|
-
}
|
|
708
|
-
const count = endIdx - startIdx + 1;
|
|
709
|
-
const range = upperBound - lowerBound;
|
|
710
|
-
if (range <= count) {
|
|
711
|
-
throw new Error(`Cannot reposition: not enough space in range (${lowerBound}, ${upperBound}) for ${count} expressions.`);
|
|
712
|
-
}
|
|
506
|
+
const { startIdx, endIdx, lowerBound, upperBound, count } = validateRepositionSiblings(children, leftPos, rightPos, this.positionConfig);
|
|
713
507
|
const modified = [];
|
|
714
508
|
const positionSet = this.childPositionsByParentId.get(parentId);
|
|
715
509
|
for (let i = startIdx; i <= endIdx; i++) {
|
|
716
|
-
positionSet?.delete(
|
|
510
|
+
positionSet?.delete(children[i].position);
|
|
717
511
|
}
|
|
718
512
|
for (let i = startIdx; i <= endIdx; i++) {
|
|
719
513
|
const newPos = Math.trunc(lowerBound +
|
|
@@ -873,88 +667,7 @@ export class ExpressionManager {
|
|
|
873
667
|
* @throws If an `implies`/`iff` expression would be inserted at a non-root position.
|
|
874
668
|
*/
|
|
875
669
|
insertExpression(expression, leftNodeId, rightNodeId) {
|
|
876
|
-
|
|
877
|
-
if (leftNodeId === undefined && rightNodeId === undefined) {
|
|
878
|
-
throw new Error(`insertExpression requires at least one of leftNodeId or rightNodeId.`);
|
|
879
|
-
}
|
|
880
|
-
// 2. The new expression's ID must not already exist.
|
|
881
|
-
if (this.expressions.has(expression.id)) {
|
|
882
|
-
throw new Error(`Expression with ID "${expression.id}" already exists.`);
|
|
883
|
-
}
|
|
884
|
-
// 3. An expression cannot be its own parent.
|
|
885
|
-
if (expression.parentId === expression.id) {
|
|
886
|
-
throw new Error(`Expression "${expression.id}" cannot be its own parent.`);
|
|
887
|
-
}
|
|
888
|
-
// 4. Left and right nodes must be distinct.
|
|
889
|
-
if (leftNodeId !== undefined &&
|
|
890
|
-
rightNodeId !== undefined &&
|
|
891
|
-
leftNodeId === rightNodeId) {
|
|
892
|
-
throw new Error(`leftNodeId and rightNodeId must be different.`);
|
|
893
|
-
}
|
|
894
|
-
// 5. The left node must exist if provided.
|
|
895
|
-
// Cast to base TExpressionInput for validation access — deferred conditional
|
|
896
|
-
// types (TExpressionInput<TExpr>) cannot be narrowed by TS control flow.
|
|
897
|
-
const leftNode = leftNodeId !== undefined
|
|
898
|
-
? this.expressions.get(leftNodeId)
|
|
899
|
-
: undefined;
|
|
900
|
-
if (leftNodeId !== undefined && !leftNode) {
|
|
901
|
-
throw new Error(`Expression "${leftNodeId}" does not exist.`);
|
|
902
|
-
}
|
|
903
|
-
// 6. The right node must exist if provided.
|
|
904
|
-
const rightNode = rightNodeId !== undefined
|
|
905
|
-
? this.expressions.get(rightNodeId)
|
|
906
|
-
: undefined;
|
|
907
|
-
if (rightNodeId !== undefined && !rightNode) {
|
|
908
|
-
throw new Error(`Expression "${rightNodeId}" does not exist.`);
|
|
909
|
-
}
|
|
910
|
-
// 7a. A variable expression cannot have children.
|
|
911
|
-
if (expression.type === "variable") {
|
|
912
|
-
throw new Error(`Variable expression "${expression.id}" cannot have children.`);
|
|
913
|
-
}
|
|
914
|
-
// 7. The "not" operator is unary and cannot take two children.
|
|
915
|
-
if (expression.type === "operator" &&
|
|
916
|
-
expression.operator === "not" &&
|
|
917
|
-
leftNodeId !== undefined &&
|
|
918
|
-
rightNodeId !== undefined) {
|
|
919
|
-
throw new Error(`Operator expression "${expression.id}" with "not" can only have one child.`);
|
|
920
|
-
}
|
|
921
|
-
// 7b. A formula expression is also unary and cannot take two children.
|
|
922
|
-
if (expression.type === "formula" &&
|
|
923
|
-
leftNodeId !== undefined &&
|
|
924
|
-
rightNodeId !== undefined) {
|
|
925
|
-
throw new Error(`Formula expression "${expression.id}" can only have one child.`);
|
|
926
|
-
}
|
|
927
|
-
// 8. The left node must not be an implies/iff expression (which must remain a root).
|
|
928
|
-
if (leftNode?.type === "operator" &&
|
|
929
|
-
(leftNode.operator === "implies" || leftNode.operator === "iff")) {
|
|
930
|
-
throw new Error(`Expression "${leftNodeId}" with "${leftNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
931
|
-
}
|
|
932
|
-
// 9. The right node must not be an implies/iff expression (which must remain a root).
|
|
933
|
-
if (rightNode?.type === "operator" &&
|
|
934
|
-
(rightNode.operator === "implies" || rightNode.operator === "iff")) {
|
|
935
|
-
throw new Error(`Expression "${rightNodeId}" with "${rightNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
936
|
-
}
|
|
937
|
-
// The anchor is the node whose current tree slot the new expression will inherit.
|
|
938
|
-
const anchor = (leftNode ?? rightNode);
|
|
939
|
-
// 10. implies/iff expressions may only be inserted at the root of the tree.
|
|
940
|
-
if (expression.type === "operator" &&
|
|
941
|
-
(expression.operator === "implies" ||
|
|
942
|
-
expression.operator === "iff") &&
|
|
943
|
-
anchor.parentId !== null) {
|
|
944
|
-
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
945
|
-
}
|
|
946
|
-
// The pre-v1.0 P-1 inline buffer-insertion / throw branches
|
|
947
|
-
// (gated on `grammarConfig.enforceFormulaBetweenOperators` +
|
|
948
|
-
// `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
|
|
949
|
-
// sites — (1) new expression as child of anchor's parent;
|
|
950
|
-
// (2) left node as child of new expression; (3) right node as
|
|
951
|
-
// child of new expression — were deleted. AN-1 (post-mutation
|
|
952
|
-
// hook in assistive mode) inserts the buffer when any of these
|
|
953
|
-
// sites produces a non-not operator under operator;
|
|
954
|
-
// permissive mode leaves the un-buffered state and
|
|
955
|
-
// `validate('presentable')` flags it.
|
|
956
|
-
const anchorParentId = anchor.parentId;
|
|
957
|
-
const anchorPosition = anchor.position;
|
|
670
|
+
const { anchorParentId, anchorPosition } = validateInsertExpression(expression, leftNodeId, rightNodeId, this.expressions);
|
|
958
671
|
// Compute child positions (midpoint-spaced for future bisection),
|
|
959
672
|
// matching the pattern used by wrapExpression. As of core 1.0.2
|
|
960
673
|
// S-8 is arity-only — `implies`/`iff` siblings may sit at any
|
|
@@ -1018,68 +731,7 @@ export class ExpressionManager {
|
|
|
1018
731
|
* @throws If the new sibling is an `implies`/`iff` operator (cannot be subordinated).
|
|
1019
732
|
*/
|
|
1020
733
|
wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
|
|
1021
|
-
|
|
1022
|
-
if (leftNodeId === undefined && rightNodeId === undefined) {
|
|
1023
|
-
throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId.`);
|
|
1024
|
-
}
|
|
1025
|
-
if (leftNodeId !== undefined && rightNodeId !== undefined) {
|
|
1026
|
-
throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId, not both.`);
|
|
1027
|
-
}
|
|
1028
|
-
// 2. Operator expression ID must not already exist.
|
|
1029
|
-
if (this.expressions.has(operator.id)) {
|
|
1030
|
-
throw new Error(`Expression with ID "${operator.id}" already exists.`);
|
|
1031
|
-
}
|
|
1032
|
-
// 3. New sibling expression ID must not already exist.
|
|
1033
|
-
if (this.expressions.has(newSibling.id)) {
|
|
1034
|
-
throw new Error(`Expression with ID "${newSibling.id}" already exists.`);
|
|
1035
|
-
}
|
|
1036
|
-
// 4. Operator and sibling IDs must be different.
|
|
1037
|
-
if (operator.id === newSibling.id) {
|
|
1038
|
-
throw new Error(`Operator and sibling expression IDs must be different.`);
|
|
1039
|
-
}
|
|
1040
|
-
// 5. The existing node must exist.
|
|
1041
|
-
const existingNodeId = (leftNodeId ?? rightNodeId);
|
|
1042
|
-
const existingNode = this.expressions.get(existingNodeId);
|
|
1043
|
-
if (!existingNode) {
|
|
1044
|
-
throw new Error(`Expression "${existingNodeId}" does not exist.`);
|
|
1045
|
-
}
|
|
1046
|
-
// 6. Operator expression must have type "operator".
|
|
1047
|
-
if (operator.type !== "operator") {
|
|
1048
|
-
throw new Error(`Wrap operator expression "${operator.id}" must have type "operator", got "${operator.type}".`);
|
|
1049
|
-
}
|
|
1050
|
-
// 7. Operator must not be unary ("not").
|
|
1051
|
-
if (operator.operator === "not") {
|
|
1052
|
-
throw new Error(`Operator expression "${operator.id}" with "not" cannot wrap (it is unary and wrapping always produces two children).`);
|
|
1053
|
-
}
|
|
1054
|
-
// 8. implies/iff operator only allowed if existing node is at root.
|
|
1055
|
-
if ((operator.operator === "implies" || operator.operator === "iff") &&
|
|
1056
|
-
existingNode.parentId !== null) {
|
|
1057
|
-
throw new Error(`Operator expression "${operator.id}" with "${operator.operator}" must be a root expression (parentId must be null).`);
|
|
1058
|
-
}
|
|
1059
|
-
// 9. Existing node must not be implies/iff (cannot be subordinated).
|
|
1060
|
-
if (existingNode.type === "operator" &&
|
|
1061
|
-
(existingNode.operator === "implies" ||
|
|
1062
|
-
existingNode.operator === "iff")) {
|
|
1063
|
-
throw new Error(`Expression "${existingNodeId}" with "${existingNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1064
|
-
}
|
|
1065
|
-
// 10. New sibling must not be implies/iff (cannot be subordinated).
|
|
1066
|
-
if (newSibling.type === "operator" &&
|
|
1067
|
-
(newSibling.operator === "implies" || newSibling.operator === "iff")) {
|
|
1068
|
-
throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1069
|
-
}
|
|
1070
|
-
// The pre-v1.0 P-1 inline buffer-insertion / throw branches
|
|
1071
|
-
// (gated on `grammarConfig.enforceFormulaBetweenOperators` +
|
|
1072
|
-
// `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
|
|
1073
|
-
// sites — (1) new operator as child of existing node's parent;
|
|
1074
|
-
// (2) existing node as child of new operator;
|
|
1075
|
-
// (3) new sibling as child of new operator — were deleted.
|
|
1076
|
-
// AN-1 (post-mutation hook in assistive mode) inserts the
|
|
1077
|
-
// buffer when any of these sites produces a non-not operator
|
|
1078
|
-
// under operator; permissive mode leaves the un-buffered state
|
|
1079
|
-
// and `validate('presentable')` flags it.
|
|
1080
|
-
// Save the existing node's slot (the operator will inherit it).
|
|
1081
|
-
const anchorParentId = existingNode.parentId;
|
|
1082
|
-
const anchorPosition = existingNode.position;
|
|
734
|
+
const { existingNodeId, anchorParentId, anchorPosition } = validateWrapExpression(operator, newSibling, leftNodeId, rightNodeId, this.expressions);
|
|
1083
735
|
// Determine child positions (midpoint-spaced for future bisection).
|
|
1084
736
|
// As of core 1.0.2 S-8 is arity-only — `implies`/`iff` siblings
|
|
1085
737
|
// may sit at any `[a, b]` with `a < b` (S-9 still guards
|
|
@@ -1279,168 +931,11 @@ export class ExpressionManager {
|
|
|
1279
931
|
*
|
|
1280
932
|
* Collects ALL violations rather than failing on the first one. Checks:
|
|
1281
933
|
* schema validity, duplicate IDs, self-referential parents, parent
|
|
1282
|
-
* existence, parent container type, root-only operators,
|
|
1283
|
-
*
|
|
1284
|
-
* checksum integrity.
|
|
934
|
+
* existence, parent container type, root-only operators, child limits,
|
|
935
|
+
* position uniqueness, and checksum integrity.
|
|
1285
936
|
*/
|
|
1286
937
|
validate() {
|
|
1287
|
-
|
|
1288
|
-
const seenIds = new Set();
|
|
1289
|
-
// ── 1. Save pre-flush checksums for later comparison ──
|
|
1290
|
-
const preFlushChecksums = new Map();
|
|
1291
|
-
for (const [id, expr] of this.expressions) {
|
|
1292
|
-
if (expr.checksum != null) {
|
|
1293
|
-
preFlushChecksums.set(id, {
|
|
1294
|
-
checksum: expr.checksum,
|
|
1295
|
-
descendantChecksum: expr.descendantChecksum,
|
|
1296
|
-
combinedChecksum: expr.combinedChecksum,
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
// ── 2. Flush checksums to get fresh values ──
|
|
1301
|
-
// Mark all expressions dirty so flush recomputes everything
|
|
1302
|
-
for (const id of this.expressions.keys()) {
|
|
1303
|
-
this.dirtyExpressionIds.add(id);
|
|
1304
|
-
}
|
|
1305
|
-
this.flushExpressionChecksums();
|
|
1306
|
-
// ── 3. Per-expression checks ──
|
|
1307
|
-
// Build a sibling-position map for position uniqueness checks
|
|
1308
|
-
const positionsByParent = new Map();
|
|
1309
|
-
for (const [id, expr] of this.expressions) {
|
|
1310
|
-
// 3a. Schema check
|
|
1311
|
-
if (!Value.Check(CorePropositionalExpressionSchema, expr)) {
|
|
1312
|
-
violations.push({
|
|
1313
|
-
code: EXPR_SCHEMA_INVALID,
|
|
1314
|
-
message: `Expression "${id}" does not conform to CorePropositionalExpressionSchema.`,
|
|
1315
|
-
entityType: "expression",
|
|
1316
|
-
entityId: id,
|
|
1317
|
-
});
|
|
1318
|
-
}
|
|
1319
|
-
// 3b. Duplicate ID
|
|
1320
|
-
if (seenIds.has(id)) {
|
|
1321
|
-
violations.push({
|
|
1322
|
-
code: EXPR_DUPLICATE_ID,
|
|
1323
|
-
message: `Duplicate expression ID "${id}".`,
|
|
1324
|
-
entityType: "expression",
|
|
1325
|
-
entityId: id,
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
seenIds.add(id);
|
|
1329
|
-
// 3c. Self-referential parent
|
|
1330
|
-
if (expr.parentId === id) {
|
|
1331
|
-
violations.push({
|
|
1332
|
-
code: EXPR_SELF_REFERENTIAL_PARENT,
|
|
1333
|
-
message: `Expression "${id}" references itself as parent.`,
|
|
1334
|
-
entityType: "expression",
|
|
1335
|
-
entityId: id,
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
// 3d. Parent existence
|
|
1339
|
-
if (expr.parentId !== null &&
|
|
1340
|
-
!this.expressions.has(expr.parentId)) {
|
|
1341
|
-
violations.push({
|
|
1342
|
-
code: EXPR_PARENT_NOT_FOUND,
|
|
1343
|
-
message: `Expression "${id}" references non-existent parent "${expr.parentId}".`,
|
|
1344
|
-
entityType: "expression",
|
|
1345
|
-
entityId: id,
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
// 3e. Parent is container (operator or formula)
|
|
1349
|
-
if (expr.parentId !== null && this.expressions.has(expr.parentId)) {
|
|
1350
|
-
const parent = this.expressions.get(expr.parentId);
|
|
1351
|
-
if (parent.type !== "operator" && parent.type !== "formula") {
|
|
1352
|
-
violations.push({
|
|
1353
|
-
code: EXPR_PARENT_NOT_CONTAINER,
|
|
1354
|
-
message: `Expression "${id}" has parent "${expr.parentId}" of type "${parent.type}" (expected operator or formula).`,
|
|
1355
|
-
entityType: "expression",
|
|
1356
|
-
entityId: id,
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
// 3f. Root-only: implies/iff must have parentId === null
|
|
1361
|
-
if (expr.type === "operator" &&
|
|
1362
|
-
(expr.operator === "implies" || expr.operator === "iff") &&
|
|
1363
|
-
expr.parentId !== null) {
|
|
1364
|
-
violations.push({
|
|
1365
|
-
code: EXPR_ROOT_ONLY_VIOLATED,
|
|
1366
|
-
message: `Root-only operator "${expr.operator}" expression "${id}" has non-null parentId "${expr.parentId}".`,
|
|
1367
|
-
entityType: "expression",
|
|
1368
|
-
entityId: id,
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
// The pre-v1.0 3g `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1372
|
-
// legacy-validate() check (gated on
|
|
1373
|
-
// `grammarConfig.enforceFormulaBetweenOperators`) is gone.
|
|
1374
|
-
// P-1 is now surfaced via the grammar-tier validators —
|
|
1375
|
-
// call `engine.validate('presentable')` and look for the
|
|
1376
|
-
// `P-1` code. The `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1377
|
-
// engine-error constant is gone in lockstep.
|
|
1378
|
-
// Collect positions for uniqueness check
|
|
1379
|
-
const parentKey = expr.parentId;
|
|
1380
|
-
let parentPositions = positionsByParent.get(parentKey);
|
|
1381
|
-
if (!parentPositions) {
|
|
1382
|
-
parentPositions = new Map();
|
|
1383
|
-
positionsByParent.set(parentKey, parentPositions);
|
|
1384
|
-
}
|
|
1385
|
-
const idsAtPosition = parentPositions.get(expr.position);
|
|
1386
|
-
if (idsAtPosition) {
|
|
1387
|
-
idsAtPosition.push(id);
|
|
1388
|
-
}
|
|
1389
|
-
else {
|
|
1390
|
-
parentPositions.set(expr.position, [id]);
|
|
1391
|
-
}
|
|
1392
|
-
// 3j. Checksum comparison
|
|
1393
|
-
const pre = preFlushChecksums.get(id);
|
|
1394
|
-
if (pre) {
|
|
1395
|
-
const fresh = this.expressions.get(id);
|
|
1396
|
-
if (pre.checksum !== fresh.checksum ||
|
|
1397
|
-
pre.descendantChecksum !== fresh.descendantChecksum ||
|
|
1398
|
-
pre.combinedChecksum !== fresh.combinedChecksum) {
|
|
1399
|
-
violations.push({
|
|
1400
|
-
code: EXPR_CHECKSUM_MISMATCH,
|
|
1401
|
-
message: `Expression "${id}" checksum mismatch: stored does not match recomputed.`,
|
|
1402
|
-
entityType: "expression",
|
|
1403
|
-
entityId: id,
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
// ── 4. Child limit checks (not/formula: max 1 child) ──
|
|
1409
|
-
for (const [id, expr] of this.expressions) {
|
|
1410
|
-
if ((expr.type === "operator" && expr.operator === "not") ||
|
|
1411
|
-
expr.type === "formula") {
|
|
1412
|
-
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
1413
|
-
const childCount = childIds?.size ?? 0;
|
|
1414
|
-
if (childCount > 1) {
|
|
1415
|
-
const label = expr.type === "formula" ? "Formula" : `Operator "not"`;
|
|
1416
|
-
violations.push({
|
|
1417
|
-
code: EXPR_CHILD_LIMIT_EXCEEDED,
|
|
1418
|
-
message: `${label} expression "${id}" has ${childCount} children (max 1).`,
|
|
1419
|
-
entityType: "expression",
|
|
1420
|
-
entityId: id,
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
// ── 5. Position uniqueness ──
|
|
1426
|
-
for (const [, posMap] of positionsByParent) {
|
|
1427
|
-
for (const [position, ids] of posMap) {
|
|
1428
|
-
if (ids.length > 1) {
|
|
1429
|
-
for (const id of ids) {
|
|
1430
|
-
violations.push({
|
|
1431
|
-
code: EXPR_POSITION_DUPLICATE,
|
|
1432
|
-
message: `Position ${position} is shared by expressions [${ids.join(", ")}].`,
|
|
1433
|
-
entityType: "expression",
|
|
1434
|
-
entityId: id,
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
return {
|
|
1441
|
-
ok: violations.length === 0,
|
|
1442
|
-
violations,
|
|
1443
|
-
};
|
|
938
|
+
return validateExpressionManagerInvariants(this.expressions, this.childExpressionIdsByParentId, this.dirtyExpressionIds, this.config, this.collector);
|
|
1444
939
|
}
|
|
1445
940
|
/** Returns a serializable snapshot of the current state. */
|
|
1446
941
|
snapshot() {
|