@proposit/proposit-core 2.3.1 → 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.
Files changed (90) hide show
  1. package/dist/cli/builder-repl.d.ts +3 -0
  2. package/dist/cli/builder-repl.d.ts.map +1 -0
  3. package/dist/cli/builder-repl.js +174 -0
  4. package/dist/cli/builder-repl.js.map +1 -0
  5. package/dist/extensions/builder/distill.d.ts +38 -0
  6. package/dist/extensions/builder/distill.d.ts.map +1 -0
  7. package/dist/extensions/builder/distill.js +81 -0
  8. package/dist/extensions/builder/distill.js.map +1 -0
  9. package/dist/extensions/builder/index.d.ts +7 -0
  10. package/dist/extensions/builder/index.d.ts.map +1 -0
  11. package/dist/extensions/builder/index.js +11 -0
  12. package/dist/extensions/builder/index.js.map +1 -0
  13. package/dist/extensions/builder/review.d.ts +11 -0
  14. package/dist/extensions/builder/review.d.ts.map +1 -0
  15. package/dist/extensions/builder/review.js +32 -0
  16. package/dist/extensions/builder/review.js.map +1 -0
  17. package/dist/extensions/builder/simulate.d.ts +12 -0
  18. package/dist/extensions/builder/simulate.d.ts.map +1 -0
  19. package/dist/extensions/builder/simulate.js +33 -0
  20. package/dist/extensions/builder/simulate.js.map +1 -0
  21. package/dist/extensions/openai/provider.d.ts.map +1 -1
  22. package/dist/extensions/openai/provider.js +3 -0
  23. package/dist/extensions/openai/provider.js.map +1 -1
  24. package/dist/extensions/openai/types.d.ts +2 -0
  25. package/dist/extensions/openai/types.d.ts.map +1 -1
  26. package/dist/lib/conversation/contract.d.ts +16 -0
  27. package/dist/lib/conversation/contract.d.ts.map +1 -0
  28. package/dist/lib/conversation/contract.js +9 -0
  29. package/dist/lib/conversation/contract.js.map +1 -0
  30. package/dist/lib/conversation/conversation.d.ts +43 -0
  31. package/dist/lib/conversation/conversation.d.ts.map +1 -0
  32. package/dist/lib/conversation/conversation.js +65 -0
  33. package/dist/lib/conversation/conversation.js.map +1 -0
  34. package/dist/lib/conversation/index.d.ts +8 -0
  35. package/dist/lib/conversation/index.d.ts.map +1 -0
  36. package/dist/lib/conversation/index.js +7 -0
  37. package/dist/lib/conversation/index.js.map +1 -0
  38. package/dist/lib/conversation/turn.d.ts +55 -0
  39. package/dist/lib/conversation/turn.d.ts.map +1 -0
  40. package/dist/lib/conversation/turn.js +112 -0
  41. package/dist/lib/conversation/turn.js.map +1 -0
  42. package/dist/lib/core/expression-manager-checks.d.ts +73 -0
  43. package/dist/lib/core/expression-manager-checks.d.ts.map +1 -0
  44. package/dist/lib/core/expression-manager-checks.js +355 -0
  45. package/dist/lib/core/expression-manager-checks.js.map +1 -0
  46. package/dist/lib/core/expression-manager-dirty-set.d.ts +21 -0
  47. package/dist/lib/core/expression-manager-dirty-set.d.ts.map +1 -0
  48. package/dist/lib/core/expression-manager-dirty-set.js +93 -0
  49. package/dist/lib/core/expression-manager-dirty-set.js.map +1 -0
  50. package/dist/lib/core/expression-manager-invariants.d.ts +14 -0
  51. package/dist/lib/core/expression-manager-invariants.d.ts.map +1 -0
  52. package/dist/lib/core/expression-manager-invariants.js +171 -0
  53. package/dist/lib/core/expression-manager-invariants.js.map +1 -0
  54. package/dist/lib/core/expression-manager.d.ts +2 -3
  55. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  56. package/dist/lib/core/expression-manager.js +18 -523
  57. package/dist/lib/core/expression-manager.js.map +1 -1
  58. package/dist/lib/index.d.ts +4 -0
  59. package/dist/lib/index.d.ts.map +1 -1
  60. package/dist/lib/index.js +6 -0
  61. package/dist/lib/index.js.map +1 -1
  62. package/dist/lib/llm/index.d.ts +1 -1
  63. package/dist/lib/llm/index.d.ts.map +1 -1
  64. package/dist/lib/llm/types.d.ts +13 -0
  65. package/dist/lib/llm/types.d.ts.map +1 -1
  66. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
  67. package/dist/lib/parsing/prompt-builder.js +13 -5
  68. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  69. package/dist/lib/pipelines/index.d.ts +4 -2
  70. package/dist/lib/pipelines/index.d.ts.map +1 -1
  71. package/dist/lib/pipelines/index.js +2 -1
  72. package/dist/lib/pipelines/index.js.map +1 -1
  73. package/dist/lib/pipelines/llm-stage-helpers.d.ts +126 -0
  74. package/dist/lib/pipelines/llm-stage-helpers.d.ts.map +1 -0
  75. package/dist/lib/pipelines/llm-stage-helpers.js +567 -0
  76. package/dist/lib/pipelines/llm-stage-helpers.js.map +1 -0
  77. package/dist/lib/pipelines/scheduler.d.ts +62 -0
  78. package/dist/lib/pipelines/scheduler.d.ts.map +1 -0
  79. package/dist/lib/pipelines/{execute.js → scheduler.js} +8 -325
  80. package/dist/lib/pipelines/scheduler.js.map +1 -0
  81. package/dist/lib/pipelines/{execute.d.ts → single-stage.d.ts} +2 -22
  82. package/dist/lib/pipelines/{execute.d.ts.map → single-stage.d.ts.map} +1 -1
  83. package/dist/lib/pipelines/single-stage.js +333 -0
  84. package/dist/lib/pipelines/single-stage.js.map +1 -0
  85. package/dist/lib/pipelines/stage-helpers.d.ts +4 -123
  86. package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
  87. package/dist/lib/pipelines/stage-helpers.js +28 -567
  88. package/dist/lib/pipelines/stage-helpers.js.map +1 -1
  89. package/package.json +13 -3
  90. 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, computeHash, canonicalSerialize } from "./checksum.js";
7
- import { Value } from "typebox/value";
8
- import { EXPR_SCHEMA_INVALID, EXPR_DUPLICATE_ID, EXPR_SELF_REFERENTIAL_PARENT, EXPR_PARENT_NOT_FOUND, EXPR_PARENT_NOT_CONTAINER, EXPR_ROOT_ONLY_VIOLATED, EXPR_CHILD_LIMIT_EXCEEDED, EXPR_POSITION_DUPLICATE, EXPR_CHECKSUM_MISMATCH, } from "../types/validation.js";
9
- const PERMITTED_OPERATOR_SWAPS = {
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
- let current = exprId;
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
- if (this.dirtyExpressionIds.size === 0)
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
- for (const id of deletedIds) {
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.get(siblingId);
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
- // Reject forbidden fields passed via `as any`.
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
- if (children.length > 1) {
556
- throw new Error(`Cannot promote: expression "${expressionId}" has multiple children (${children.length}). Use deleteSubtree: true or remove children first.`);
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 positions = children.map((c) => c.position);
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(positions[i]);
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
- // 1. At least one child node must be provided.
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
- // 1. Exactly one of leftNodeId / rightNodeId must be provided.
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, formula-between-
1283
- * operators (when enabled), child limits, position uniqueness, and
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
- const violations = [];
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() {