@proposit/proposit-core 0.12.3 → 1.0.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 (125) hide show
  1. package/README.md +104 -93
  2. package/dist/cli/commands/premises.d.ts.map +1 -1
  3. package/dist/cli/commands/premises.js +28 -24
  4. package/dist/cli/commands/premises.js.map +1 -1
  5. package/dist/cli/commands/repair.d.ts.map +1 -1
  6. package/dist/cli/commands/repair.js +4 -2
  7. package/dist/cli/commands/repair.js.map +1 -1
  8. package/dist/cli/commands/validate.d.ts.map +1 -1
  9. package/dist/cli/commands/validate.js +7 -1
  10. package/dist/cli/commands/validate.js.map +1 -1
  11. package/dist/cli/engine.d.ts.map +1 -1
  12. package/dist/cli/engine.js +7 -25
  13. package/dist/cli/engine.js.map +1 -1
  14. package/dist/cli/import.d.ts.map +1 -1
  15. package/dist/cli/import.js +66 -28
  16. package/dist/cli/import.js.map +1 -1
  17. package/dist/lib/core/argument-engine.d.ts +275 -10
  18. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  19. package/dist/lib/core/argument-engine.js +442 -82
  20. package/dist/lib/core/argument-engine.js.map +1 -1
  21. package/dist/lib/core/argument-library.d.ts +5 -1
  22. package/dist/lib/core/argument-library.d.ts.map +1 -1
  23. package/dist/lib/core/argument-library.js +7 -3
  24. package/dist/lib/core/argument-library.js.map +1 -1
  25. package/dist/lib/core/expression-manager.d.ts +68 -73
  26. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  27. package/dist/lib/core/expression-manager.js +272 -771
  28. package/dist/lib/core/expression-manager.js.map +1 -1
  29. package/dist/lib/core/fork.d.ts +5 -1
  30. package/dist/lib/core/fork.d.ts.map +1 -1
  31. package/dist/lib/core/fork.js +16 -6
  32. package/dist/lib/core/fork.js.map +1 -1
  33. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +68 -7
  34. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  35. package/dist/lib/core/interfaces/library.interfaces.d.ts +8 -3
  36. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  37. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +50 -47
  38. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  39. package/dist/lib/core/premise-engine.d.ts +80 -11
  40. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  41. package/dist/lib/core/premise-engine.js +245 -83
  42. package/dist/lib/core/premise-engine.js.map +1 -1
  43. package/dist/lib/core/proposit-core.d.ts.map +1 -1
  44. package/dist/lib/core/proposit-core.js +13 -3
  45. package/dist/lib/core/proposit-core.js.map +1 -1
  46. package/dist/lib/grammar/an-rules.d.ts +158 -0
  47. package/dist/lib/grammar/an-rules.d.ts.map +1 -0
  48. package/dist/lib/grammar/an-rules.js +778 -0
  49. package/dist/lib/grammar/an-rules.js.map +1 -0
  50. package/dist/lib/grammar/auto-normalize.d.ts +14 -0
  51. package/dist/lib/grammar/auto-normalize.d.ts.map +1 -0
  52. package/dist/lib/grammar/auto-normalize.js +35 -0
  53. package/dist/lib/grammar/auto-normalize.js.map +1 -0
  54. package/dist/lib/grammar/bounded-subtree.d.ts +30 -0
  55. package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -0
  56. package/dist/lib/grammar/bounded-subtree.js +74 -0
  57. package/dist/lib/grammar/bounded-subtree.js.map +1 -0
  58. package/dist/lib/grammar/naked-q.d.ts +20 -0
  59. package/dist/lib/grammar/naked-q.d.ts.map +1 -0
  60. package/dist/lib/grammar/naked-q.js +58 -0
  61. package/dist/lib/grammar/naked-q.js.map +1 -0
  62. package/dist/lib/grammar/normalize.d.ts +12 -0
  63. package/dist/lib/grammar/normalize.d.ts.map +1 -0
  64. package/dist/lib/grammar/normalize.js +45 -0
  65. package/dist/lib/grammar/normalize.js.map +1 -0
  66. package/dist/lib/grammar/populate-from.d.ts +25 -0
  67. package/dist/lib/grammar/populate-from.d.ts.map +1 -0
  68. package/dist/lib/grammar/populate-from.js +252 -0
  69. package/dist/lib/grammar/populate-from.js.map +1 -0
  70. package/dist/lib/grammar/repair.d.ts +65 -0
  71. package/dist/lib/grammar/repair.d.ts.map +1 -0
  72. package/dist/lib/grammar/repair.js +251 -0
  73. package/dist/lib/grammar/repair.js.map +1 -0
  74. package/dist/lib/grammar/types.d.ts +17 -0
  75. package/dist/lib/grammar/types.d.ts.map +1 -0
  76. package/dist/lib/grammar/types.js +82 -0
  77. package/dist/lib/grammar/types.js.map +1 -0
  78. package/dist/lib/grammar/validate.d.ts +4 -0
  79. package/dist/lib/grammar/validate.d.ts.map +1 -0
  80. package/dist/lib/grammar/validate.js +28 -0
  81. package/dist/lib/grammar/validate.js.map +1 -0
  82. package/dist/lib/grammar/validators/context.d.ts +11 -0
  83. package/dist/lib/grammar/validators/context.d.ts.map +1 -0
  84. package/dist/lib/grammar/validators/context.js +5 -0
  85. package/dist/lib/grammar/validators/context.js.map +1 -0
  86. package/dist/lib/grammar/validators/derivable.d.ts +63 -0
  87. package/dist/lib/grammar/validators/derivable.d.ts.map +1 -0
  88. package/dist/lib/grammar/validators/derivable.js +502 -0
  89. package/dist/lib/grammar/validators/derivable.js.map +1 -0
  90. package/dist/lib/grammar/validators/evaluable.d.ts +48 -0
  91. package/dist/lib/grammar/validators/evaluable.d.ts.map +1 -0
  92. package/dist/lib/grammar/validators/evaluable.js +226 -0
  93. package/dist/lib/grammar/validators/evaluable.js.map +1 -0
  94. package/dist/lib/grammar/validators/presentable.d.ts +45 -0
  95. package/dist/lib/grammar/validators/presentable.d.ts.map +1 -0
  96. package/dist/lib/grammar/validators/presentable.js +231 -0
  97. package/dist/lib/grammar/validators/presentable.js.map +1 -0
  98. package/dist/lib/grammar/validators/structural.d.ts +103 -0
  99. package/dist/lib/grammar/validators/structural.d.ts.map +1 -0
  100. package/dist/lib/grammar/validators/structural.js +602 -0
  101. package/dist/lib/grammar/validators/structural.js.map +1 -0
  102. package/dist/lib/index.d.ts +4 -3
  103. package/dist/lib/index.d.ts.map +1 -1
  104. package/dist/lib/index.js +2 -2
  105. package/dist/lib/index.js.map +1 -1
  106. package/dist/lib/parsing/argument-parser.d.ts.map +1 -1
  107. package/dist/lib/parsing/argument-parser.js +52 -10
  108. package/dist/lib/parsing/argument-parser.js.map +1 -1
  109. package/dist/lib/types/evaluation.d.ts +1 -1
  110. package/dist/lib/types/evaluation.d.ts.map +1 -1
  111. package/dist/lib/types/fork.d.ts +12 -3
  112. package/dist/lib/types/fork.d.ts.map +1 -1
  113. package/dist/lib/types/validation.d.ts +0 -6
  114. package/dist/lib/types/validation.d.ts.map +1 -1
  115. package/dist/lib/types/validation.js +23 -6
  116. package/dist/lib/types/validation.js.map +1 -1
  117. package/package.json +1 -1
  118. package/dist/lib/core/managed-derivation-premise-engine.d.ts +0 -172
  119. package/dist/lib/core/managed-derivation-premise-engine.d.ts.map +0 -1
  120. package/dist/lib/core/managed-derivation-premise-engine.js +0 -550
  121. package/dist/lib/core/managed-derivation-premise-engine.js.map +0 -1
  122. package/dist/lib/types/grammar.d.ts +0 -83
  123. package/dist/lib/types/grammar.d.ts.map +0 -1
  124. package/dist/lib/types/grammar.js +0 -24
  125. package/dist/lib/types/grammar.js.map +0 -1
@@ -4,9 +4,8 @@ import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
4
4
  import { defaultGenerateId } from "./argument-engine.js";
5
5
  import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
6
6
  import { entityChecksum, computeHash, canonicalSerialize } from "./checksum.js";
7
- import { DEFAULT_GRAMMAR_CONFIG, resolveAutoNormalize, } from "../types/grammar.js";
8
7
  import { Value } from "typebox/value";
9
- import { EXPR_SCHEMA_INVALID, EXPR_DUPLICATE_ID, EXPR_SELF_REFERENTIAL_PARENT, EXPR_PARENT_NOT_FOUND, EXPR_PARENT_NOT_CONTAINER, EXPR_ROOT_ONLY_VIOLATED, EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED, EXPR_CHILD_LIMIT_EXCEEDED, EXPR_POSITION_DUPLICATE, EXPR_CHECKSUM_MISMATCH, } from "../types/validation.js";
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";
10
9
  const PERMITTED_OPERATOR_SWAPS = {
11
10
  and: "or",
12
11
  or: "and",
@@ -36,15 +35,6 @@ export class ExpressionManager {
36
35
  setCollector(collector) {
37
36
  this.collector = collector;
38
37
  }
39
- /**
40
- * Overrides the grammar config used for validation and mutation-time
41
- * checks. Called by restoration paths (e.g. `fromSnapshot`) when the
42
- * caller supplies a grammar config that should override whatever was
43
- * stored in the snapshot.
44
- */
45
- setGrammarConfig(grammarConfig) {
46
- this.config = { ...this.config, grammarConfig };
47
- }
48
38
  constructor(config) {
49
39
  this.expressions = new Map();
50
40
  this.childExpressionIdsByParentId = new Map();
@@ -53,8 +43,17 @@ export class ExpressionManager {
53
43
  this.config = config;
54
44
  this.generateId = config?.generateId ?? defaultGenerateId;
55
45
  }
56
- get grammarConfig() {
57
- return this.config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
46
+ /**
47
+ * Returns the position config in effect for this expression manager.
48
+ * Used by in-package helpers (e.g. native AN-4 in
49
+ * `src/lib/grammar/an-rules.ts`) that need the position range
50
+ * boundaries (`min`/`max`) for spacing-algorithm fallbacks when a
51
+ * formula sits at the leftmost or rightmost slot under its parent.
52
+ *
53
+ * @internal
54
+ */
55
+ getPositionConfig() {
56
+ return this.positionConfig;
58
57
  }
59
58
  attachChecksum(expr) {
60
59
  const fields = this.config?.checksumConfig?.expressionFields ??
@@ -88,9 +87,13 @@ export class ExpressionManager {
88
87
  * maps (`expressions`, `childExpressionIdsByParentId`,
89
88
  * `childPositionsByParentId`) and notifies the change collector.
90
89
  *
91
- * Used by `addExpression`, `insertExpression`, and `wrapExpression` to
92
- * auto-insert formula nodes between operators when
93
- * `grammarConfig.autoNormalize` is enabled.
90
+ * As of v1.0 the legacy per-mutation P-1 buffer-insertion branches
91
+ * (`addExpression`/`insertExpression`/`wrapExpression`) are gone
92
+ * AN-1 (post-mutation hook in assistive mode, see
93
+ * `src/lib/grammar/an-rules.ts`) is the sole formula-buffer
94
+ * insertion path. This helper is invoked from `wrapInFormula` (the
95
+ * public AN-1 primitive on `PremiseEngine`) which calls it once per
96
+ * buffer insertion to materialize the formula node.
94
97
  *
95
98
  * @returns The generated formula expression ID.
96
99
  */
@@ -239,7 +242,7 @@ export class ExpressionManager {
239
242
  * @throws If the position is already occupied under the parent.
240
243
  */
241
244
  addExpression(input) {
242
- let expression = input;
245
+ const expression = input;
243
246
  if (this.expressions.has(expression.id)) {
244
247
  throw new Error(`Expression with ID "${expression.id}" already exists.`);
245
248
  }
@@ -253,36 +256,20 @@ export class ExpressionManager {
253
256
  throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
254
257
  }
255
258
  if (expression.parentId !== null) {
256
- let parent = this.expressions.get(expression.parentId);
259
+ const parent = this.expressions.get(expression.parentId);
257
260
  if (!parent) {
258
261
  throw new Error(`Parent expression "${expression.parentId}" does not exist.`);
259
262
  }
260
263
  if (parent.type !== "operator" && parent.type !== "formula") {
261
264
  throw new Error(`Parent expression "${expression.parentId}" is not an operator expression.`);
262
265
  }
263
- // Non-not operators cannot be direct children of operators.
264
- if (this.grammarConfig.enforceFormulaBetweenOperators &&
265
- parent.type === "operator" &&
266
- expression.type === "operator" &&
267
- expression.operator !== "not") {
268
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
269
- // Check original parent can accept the formula as a new child.
270
- this.assertChildLimit(parent.operator, expression.parentId);
271
- // Auto-insert a formula buffer between parent and expression.
272
- const formulaId = this.registerFormulaBuffer(expression, expression.parentId, expression.position);
273
- // Rewrite expression to be child of formula.
274
- expression = {
275
- ...expression,
276
- parentId: formulaId,
277
- position: 0,
278
- };
279
- // Update parent reference for subsequent checks.
280
- parent = this.expressions.get(formulaId);
281
- }
282
- else {
283
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
284
- }
285
- }
266
+ // D2: P-1 (non-not operator under operator) is no longer
267
+ // enforced at mutation time. Assistive mode inserts the
268
+ // formula buffer via AN-1 post-hook; permissive mode
269
+ // leaves the un-buffered state and `validate('presentable')`
270
+ // flags it. The pre-v1.0 inline buffer-insertion fallback +
271
+ // throw both lived here under `grammarConfig.enforceFormula
272
+ // BetweenOperators` deleted in D2.
286
273
  if (parent.type === "operator") {
287
274
  this.assertChildLimit(parent.operator, expression.parentId);
288
275
  }
@@ -320,12 +307,14 @@ export class ExpressionManager {
320
307
  const lastChild = children[children.length - 1];
321
308
  let position = midpoint(lastChild.position, this.positionConfig.max);
322
309
  if (position === lastChild.position) {
323
- if (resolveAutoNormalize(this.grammarConfig, "repositionOnCollision")) {
324
- this.repositionSiblings(parentId, lastChild.position, this.positionConfig.max);
325
- const updated = this.getChildExpressions(parentId);
326
- const newLast = updated[updated.length - 1];
327
- position = midpoint(newLast.position, this.positionConfig.max);
328
- }
310
+ // Composite-mutation behavior (spec §8 / S-9): always shift
311
+ // colliding siblings as part of the bundled op. The pre-v1.0
312
+ // `repositionOnCollision` flag gating is gone in D2 — composites
313
+ // never leave a Structural violation by design.
314
+ this.repositionSiblings(parentId, lastChild.position, this.positionConfig.max);
315
+ const updated = this.getChildExpressions(parentId);
316
+ const newLast = updated[updated.length - 1];
317
+ position = midpoint(newLast.position, this.positionConfig.max);
329
318
  }
330
319
  this.addExpression({
331
320
  ...expression,
@@ -352,17 +341,19 @@ export class ExpressionManager {
352
341
  : this.positionConfig.min;
353
342
  position = midpoint(prevPosition, sibling.position);
354
343
  if (position === prevPosition || position === sibling.position) {
355
- if (resolveAutoNormalize(this.grammarConfig, "repositionOnCollision")) {
356
- this.repositionSiblings(sibling.parentId, siblingIndex > 0
357
- ? children[siblingIndex - 1].position
358
- : this.positionConfig.min, sibling.position);
359
- const updated = this.getChildExpressions(sibling.parentId);
360
- const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
361
- const newPrevPos = newSiblingIdx > 0
362
- ? updated[newSiblingIdx - 1].position
363
- : this.positionConfig.min;
364
- position = midpoint(newPrevPos, updated[newSiblingIdx].position);
365
- }
344
+ // Composite-mutation behavior (spec §8 / S-9): always shift
345
+ // colliding siblings as part of the bundled op. The
346
+ // pre-v1.0 `repositionOnCollision` flag gating is gone in
347
+ // D2.
348
+ this.repositionSiblings(sibling.parentId, siblingIndex > 0
349
+ ? children[siblingIndex - 1].position
350
+ : this.positionConfig.min, sibling.position);
351
+ const updated = this.getChildExpressions(sibling.parentId);
352
+ const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
353
+ const newPrevPos = newSiblingIdx > 0
354
+ ? updated[newSiblingIdx - 1].position
355
+ : this.positionConfig.min;
356
+ position = midpoint(newPrevPos, updated[newSiblingIdx].position);
366
357
  }
367
358
  }
368
359
  else {
@@ -371,17 +362,17 @@ export class ExpressionManager {
371
362
  : this.positionConfig.max;
372
363
  position = midpoint(sibling.position, nextPosition);
373
364
  if (position === sibling.position || position === nextPosition) {
374
- if (resolveAutoNormalize(this.grammarConfig, "repositionOnCollision")) {
375
- this.repositionSiblings(sibling.parentId, sibling.position, siblingIndex < children.length - 1
376
- ? children[siblingIndex + 1].position
377
- : this.positionConfig.max);
378
- const updated = this.getChildExpressions(sibling.parentId);
379
- const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
380
- const newNextPos = newSiblingIdx < updated.length - 1
381
- ? updated[newSiblingIdx + 1].position
382
- : this.positionConfig.max;
383
- position = midpoint(updated[newSiblingIdx].position, newNextPos);
384
- }
365
+ // Composite-mutation behavior (spec §8 / S-9): always
366
+ // shift colliding siblings as part of the bundled op.
367
+ this.repositionSiblings(sibling.parentId, sibling.position, siblingIndex < children.length - 1
368
+ ? children[siblingIndex + 1].position
369
+ : this.positionConfig.max);
370
+ const updated = this.getChildExpressions(sibling.parentId);
371
+ const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
372
+ const newNextPos = newSiblingIdx < updated.length - 1
373
+ ? updated[newSiblingIdx + 1].position
374
+ : this.positionConfig.max;
375
+ position = midpoint(updated[newSiblingIdx].position, newNextPos);
385
376
  }
386
377
  }
387
378
  this.addExpression({
@@ -481,23 +472,26 @@ export class ExpressionManager {
481
472
  });
482
473
  // Mark the updated expression and its ancestors dirty for hierarchical checksum recomputation.
483
474
  this.markExpressionDirty(expressionId);
484
- // After an operator swap, absorb same-operator children through a formula.
485
- if (updates.operator !== undefined) {
486
- this.absorbSameOperatorIfNeeded(expressionId);
487
- }
475
+ // D2: the pre-v1.0 same-operator absorption inline cascade
476
+ // (gated on `absorbSameOperator`) is gone. AN-4 (post-mutation
477
+ // hook in assistive mode) handles same-operator absorption
478
+ // through a formula buffer.
488
479
  return this.expressions.get(expressionId) ?? updated;
489
480
  }
490
481
  /**
491
482
  * Removes an expression from the tree.
492
483
  *
493
484
  * When `deleteSubtree` is `true`, the expression and its entire descendant
494
- * subtree are removed, then {@link collapseIfNeeded} runs on the parent.
485
+ * subtree are removed.
495
486
  *
496
487
  * When `deleteSubtree` is `false`, the expression is removed but its single
497
- * child (if any) is promoted into the removed expression's slot. If the
498
- * expression has more than one child, an error is thrown. Leaf removal
499
- * (0 children) still triggers {@link collapseIfNeeded} on the parent.
500
- * Promotion does **not** trigger collapse.
488
+ * child (if any) is promoted into the removed expression's slot. If the
489
+ * expression has more than one child, an error is thrown.
490
+ *
491
+ * As of v1.0 the pre-removal collapse-cascade (the legacy
492
+ * `collapseIfNeeded` / `simulateCollapseChain`) is gone — AN-3
493
+ * (post-mutation hook in assistive mode) handles 0/1-child
494
+ * operator/formula collapse on the surviving parent.
501
495
  *
502
496
  * @throws If `deleteSubtree` is `false` and the expression has multiple children.
503
497
  * @throws If `deleteSubtree` is `false` and the single child is a root-only
@@ -551,7 +545,10 @@ export class ExpressionManager {
551
545
  if (parentId !== null) {
552
546
  this.markExpressionDirty(parentId);
553
547
  }
554
- this.collapseIfNeeded(parentId);
548
+ // D2: the pre-v1.0 `collapseIfNeeded(parentId)` inline cascade
549
+ // (gated on `collapseEmptyFormula`) is gone. AN-3 (post-mutation
550
+ // hook in assistive mode) handles 0/1-child operator/formula
551
+ // collapse.
555
552
  return target;
556
553
  }
557
554
  removeAndPromote(expressionId, target) {
@@ -560,7 +557,7 @@ export class ExpressionManager {
560
557
  throw new Error(`Cannot promote: expression "${expressionId}" has multiple children (${children.length}). Use deleteSubtree: true or remove children first.`);
561
558
  }
562
559
  if (children.length === 0) {
563
- // Leaf removal — same as removing a single node, then collapse parent.
560
+ // Leaf removal.
564
561
  const parentId = target.parentId;
565
562
  this.collector?.removedExpression({
566
563
  ...target,
@@ -571,22 +568,17 @@ export class ExpressionManager {
571
568
  if (parentId !== null) {
572
569
  this.markExpressionDirty(parentId);
573
570
  }
574
- this.collapseIfNeeded(parentId);
571
+ // D2: AN-3 (post-mutation hook in assistive mode) handles
572
+ // 0/1-child operator/formula collapse on the parent.
575
573
  return target;
576
574
  }
577
575
  // Exactly 1 child — promote it into the target's slot.
578
576
  const child = children[0];
579
- // Validate: non-not operators cannot be promoted into an operator parent.
580
- if (this.grammarConfig.enforceFormulaBetweenOperators) {
581
- if (child.type === "operator" &&
582
- child.operator !== "not" &&
583
- target.parentId !== null) {
584
- const grandparent = this.expressions.get(target.parentId);
585
- if (grandparent && grandparent.type === "operator") {
586
- throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
587
- }
588
- }
589
- }
577
+ // D2: the P-1 promote-on-remove enforcement throw lived here under
578
+ // `grammarConfig.enforceFormulaBetweenOperators`. AN-1 (post-mutation
579
+ // hook in assistive mode) now inserts the buffer if the promotion
580
+ // produced a non-not operator under operator; permissive mode leaves
581
+ // the un-buffered state and `validate('presentable')` flags it.
590
582
  // Validate: root-only operators cannot be promoted into a non-root position.
591
583
  if (child.type === "operator" &&
592
584
  (child.operator === "implies" || child.operator === "iff") &&
@@ -623,60 +615,18 @@ export class ExpressionManager {
623
615
  // (its parentId changed) which also propagates to ancestors.
624
616
  this.dirtyExpressionIds.delete(expressionId);
625
617
  this.markExpressionDirty(child.id);
626
- // After promotion, the target's parent may be a formula that now needs collapsing
627
- // (e.g., if the promoted child has no binary operator in its bounded subtree).
628
- this.collapseIfNeeded(target.parentId);
618
+ // D2: AN-3 (post-mutation hook in assistive mode) handles
619
+ // formula collapse on the target's parent if the promoted child
620
+ // makes the formula unjustified.
629
621
  return target;
630
622
  }
631
- /**
632
- * Promotes `child` into the slot occupied by `parent` and removes `parent`.
633
- * Used by `collapseIfNeeded` and `normalize()`.
634
- */
635
- promoteChild(parentId, parent, child) {
636
- const grandparentId = parent.parentId;
637
- let promotedPosition = parent.position;
638
- // When repositionOnCollision is enabled and the grandparent has multiple
639
- // children, compute the midpoint between the parent's left and right
640
- // neighbors for better spacing.
641
- if (grandparentId !== null &&
642
- resolveAutoNormalize(this.grammarConfig, "repositionOnCollision")) {
643
- const siblings = this.getChildExpressions(grandparentId);
644
- if (siblings.length > 1) {
645
- const parentIdx = siblings.findIndex((s) => s.id === parentId);
646
- const leftPos = parentIdx > 0
647
- ? siblings[parentIdx - 1].position
648
- : this.positionConfig.min;
649
- const rightPos = parentIdx < siblings.length - 1
650
- ? siblings[parentIdx + 1].position
651
- : this.positionConfig.max;
652
- promotedPosition = midpoint(leftPos, rightPos);
653
- }
654
- }
655
- const promoted = this.attachChecksum({
656
- ...child,
657
- parentId: grandparentId,
658
- position: promotedPosition,
659
- });
660
- this.expressions.set(child.id, promoted);
661
- this.collector?.modifiedExpression({
662
- ...promoted,
663
- });
664
- this.childExpressionIdsByParentId.get(grandparentId)?.delete(parentId);
665
- getOrCreate(this.childExpressionIdsByParentId, grandparentId, () => new Set()).add(child.id);
666
- // Update position tracking: remove parent's old position, add promoted position.
667
- this.childPositionsByParentId
668
- .get(grandparentId)
669
- ?.delete(parent.position);
670
- getOrCreate(this.childPositionsByParentId, grandparentId, () => new Set()).add(promotedPosition);
671
- this.childExpressionIdsByParentId.delete(parentId);
672
- this.childPositionsByParentId.delete(parentId);
673
- this.collector?.removedExpression({
674
- ...parent,
675
- });
676
- this.expressions.delete(parentId);
677
- this.dirtyExpressionIds.delete(parentId);
678
- this.markExpressionDirty(child.id);
679
- }
623
+ // D2 — `promoteChild` was the helper for the pre-v1.0 inline
624
+ // collapse cascade (`collapseIfNeeded`) and the legacy
625
+ // `ExpressionManager.normalize()` sweep, both of which are gone in
626
+ // D2 (replaced by AN-3 / AN-4 / AN-2 / AN-1 post-mutation hooks in
627
+ // `src/lib/grammar/an-rules.ts`). The remaining
628
+ // `removeAndPromote` 1-child branch in `removeExpression` writes
629
+ // the promoted child directly (no shared helper is needed).
680
630
  /**
681
631
  * Redistributes the minimal set of sibling positions to create room at
682
632
  * an insertion point between `leftPos` and `rightPos` under `parentId`.
@@ -786,374 +736,21 @@ export class ExpressionManager {
786
736
  }
787
737
  return modified;
788
738
  }
789
- collapseIfNeeded(operatorId) {
790
- if (!resolveAutoNormalize(this.grammarConfig, "collapseEmptyFormula"))
791
- return;
792
- if (operatorId === null)
793
- return;
794
- const operator = this.expressions.get(operatorId);
795
- if (!operator)
796
- return;
797
- if (operator.type === "formula") {
798
- const children = this.getChildExpressions(operatorId);
799
- if (children.length === 0) {
800
- const grandparentId = operator.parentId;
801
- this.collector?.removedExpression({
802
- ...operator,
803
- });
804
- this.detachExpression(operatorId, operator);
805
- this.dirtyExpressionIds.delete(operatorId);
806
- if (grandparentId !== null) {
807
- this.markExpressionDirty(grandparentId);
808
- }
809
- this.collapseIfNeeded(grandparentId);
810
- return;
811
- }
812
- // 1-child formula: collapse if no binary operator in bounded subtree.
813
- if (children.length === 1 &&
814
- !this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
815
- const grandparentId = operator.parentId;
816
- this.promoteChild(operatorId, operator, children[0]);
817
- // Grandparent may also be a formula that now needs collapsing.
818
- this.collapseIfNeeded(grandparentId);
819
- }
820
- return;
821
- }
822
- if (operator.type !== "operator")
823
- return;
824
- const children = this.getChildExpressions(operatorId);
825
- if (children.length === 0) {
826
- const grandparentId = operator.parentId;
827
- this.collector?.removedExpression({
828
- ...operator,
829
- });
830
- this.detachExpression(operatorId, operator);
831
- // Prune collapsed operator from dirty set and propagate to grandparent.
832
- this.dirtyExpressionIds.delete(operatorId);
833
- if (grandparentId !== null) {
834
- this.markExpressionDirty(grandparentId);
835
- }
836
- this.collapseIfNeeded(grandparentId);
837
- }
838
- else if (children.length === 1 && operator.operator === "not") {
839
- // `not` is unary — 1 child is its valid state; skip collapse.
840
- // Still recurse to grandparent: a formula wrapping this `not` may
841
- // now qualify for collapse after a descendant change.
842
- this.collapseIfNeeded(operator.parentId);
843
- }
844
- else if (children.length === 1) {
845
- const child = children[0];
846
- const grandparentId = operator.parentId;
847
- // Defense-in-depth: validate promotion doesn't violate nesting or root-only rules.
848
- if (child.type === "operator") {
849
- // Root-only — always enforced
850
- if ((child.operator === "implies" ||
851
- child.operator === "iff") &&
852
- grandparentId !== null) {
853
- throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
854
- }
855
- // Nesting — grammar-configurable
856
- if (this.grammarConfig.enforceFormulaBetweenOperators) {
857
- if (child.operator !== "not" && grandparentId !== null) {
858
- const grandparent = this.expressions.get(grandparentId);
859
- if (grandparent && grandparent.type === "operator") {
860
- throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
861
- }
862
- }
863
- }
864
- }
865
- this.promoteChild(operatorId, operator, child);
866
- // Grandparent may be a formula that now needs collapsing after the
867
- // promoted child replaced the operator.
868
- this.collapseIfNeeded(grandparentId);
869
- }
870
- }
871
- /**
872
- * Flag-gated wrapper: absorbs same-operator children through a formula
873
- * after an operator swap, if `absorbSameOperator` is enabled.
874
- */
875
- absorbSameOperatorIfNeeded(expressionId) {
876
- if (!resolveAutoNormalize(this.grammarConfig, "absorbSameOperator"))
877
- return;
878
- this.absorbSameOperator(expressionId);
879
- }
880
- /**
881
- * Absorbs the inner operator's children into the outer (grandparent)
882
- * operator when both are the same type, separated by a formula.
883
- *
884
- * Example: `AND → formula → AND(B, C)` becomes `AND → [B, C]` (children
885
- * promoted into the outer AND at positions between the formula's neighbors).
886
- *
887
- * No-op if the pattern does not match.
888
- */
889
- absorbSameOperator(expressionId) {
890
- const inner = this.expressions.get(expressionId);
891
- if (!inner || inner.type !== "operator")
892
- return false;
893
- // Only and/or are absorbable (implies/iff are root-only).
894
- if (inner.operator !== "and" && inner.operator !== "or")
895
- return false;
896
- const formulaId = inner.parentId;
897
- if (formulaId === null)
898
- return false;
899
- const formula = this.expressions.get(formulaId);
900
- if (!formula || formula.type !== "formula")
901
- return false;
902
- const outerId = formula.parentId;
903
- if (outerId === null)
904
- return false;
905
- const outer = this.expressions.get(outerId);
906
- if (!outer || outer.type !== "operator")
907
- return false;
908
- if (outer.operator !== inner.operator)
909
- return false;
910
- // Same operator on both sides of a formula — absorb.
911
- const innerChildren = this.getChildExpressions(expressionId);
912
- // Determine the position gap available for absorbed children.
913
- const outerChildren = this.getChildExpressions(outerId);
914
- const formulaIdx = outerChildren.findIndex((c) => c.id === formulaId);
915
- const leftPos = formulaIdx > 0
916
- ? outerChildren[formulaIdx - 1].position
917
- : this.positionConfig.min;
918
- const rightPos = formulaIdx < outerChildren.length - 1
919
- ? outerChildren[formulaIdx + 1].position
920
- : this.positionConfig.max;
921
- // Check if there is enough integer space between neighbors for N children.
922
- const gap = rightPos - leftPos;
923
- const count = innerChildren.length;
924
- const needsRedistribution = gap <= count;
925
- // Reparent each inner child to the outer operator. Use evenly spaced
926
- // positions within the gap; these may collide if the gap is too tight,
927
- // which is fixed by a full redistribution below.
928
- for (let i = 0; i < count; i++) {
929
- const newPosition = Math.trunc(leftPos + ((rightPos - leftPos) / (count + 1)) * (i + 1));
930
- this.reparent(innerChildren[i].id, outerId, newPosition);
931
- }
932
- // Remove the inner operator and formula (now childless).
933
- this.childExpressionIdsByParentId.delete(expressionId);
934
- this.childPositionsByParentId.delete(expressionId);
935
- this.collector?.removedExpression({
936
- ...inner,
937
- });
938
- this.expressions.delete(expressionId);
939
- this.dirtyExpressionIds.delete(expressionId);
940
- this.childExpressionIdsByParentId.get(outerId)?.delete(formulaId);
941
- this.childPositionsByParentId.get(outerId)?.delete(formula.position);
942
- this.childExpressionIdsByParentId.delete(formulaId);
943
- this.childPositionsByParentId.delete(formulaId);
944
- this.collector?.removedExpression({
945
- ...formula,
946
- });
947
- this.expressions.delete(formulaId);
948
- this.dirtyExpressionIds.delete(formulaId);
949
- // If the gap was too tight, redistribute all children of the outer
950
- // operator evenly across the full position range.
951
- if (needsRedistribution) {
952
- const allChildren = this.getChildExpressions(outerId);
953
- const total = allChildren.length;
954
- const positionSet = this.childPositionsByParentId.get(outerId);
955
- for (const child of allChildren) {
956
- positionSet?.delete(child.position);
957
- }
958
- for (let i = 0; i < total; i++) {
959
- const newPos = Math.trunc(this.positionConfig.min +
960
- ((this.positionConfig.max - this.positionConfig.min) /
961
- (total + 1)) *
962
- (i + 1));
963
- const updated = this.attachChecksum({
964
- ...allChildren[i],
965
- position: newPos,
966
- });
967
- this.expressions.set(allChildren[i].id, updated);
968
- this.collector?.modifiedExpression({
969
- ...updated,
970
- });
971
- positionSet?.add(newPos);
972
- this.markExpressionDirty(allChildren[i].id);
973
- }
974
- }
975
- // Mark the outer operator dirty.
976
- this.markExpressionDirty(outerId);
977
- return true;
978
- }
979
- /**
980
- * Checks whether the subtree rooted at `expressionId` contains a binary
981
- * operator (`and` or `or`). Traversal stops at formula boundaries — a
982
- * nested formula owns its own subtree and is not inspected.
983
- */
984
- hasBinaryOperatorInBoundedSubtree(expressionId) {
985
- const expr = this.expressions.get(expressionId);
986
- if (!expr)
987
- return false;
988
- if (expr.type === "formula")
989
- return false;
990
- if (expr.type === "variable")
991
- return false;
992
- if (expr.type === "operator" &&
993
- (expr.operator === "and" || expr.operator === "or")) {
994
- return true;
995
- }
996
- const children = this.getChildExpressions(expressionId);
997
- return children.some((child) => this.hasBinaryOperatorInBoundedSubtree(child.id));
998
- }
999
- /**
1000
- * Performs a full normalization sweep on the expression tree:
1001
- * 1. Collapses operators with 0 or 1 children.
1002
- * 2. Collapses formulas whose bounded subtree has no binary operator.
1003
- * 3. Inserts formula buffers where `enforceFormulaBetweenOperators` requires them.
1004
- * 4. Collapses double negation.
1005
- * 5. Absorbs same-operator nesting through formulas.
1006
- * 6. Repeats until stable.
1007
- *
1008
- * Works regardless of the current `autoNormalize` setting — this is an
1009
- * explicit on-demand normalization.
1010
- */
1011
- normalize() {
1012
- let changed = true;
1013
- while (changed) {
1014
- changed = false;
1015
- // Pass 1: Collapse operators with 0 or 1 children (bottom-up).
1016
- for (const expr of this.toArray()) {
1017
- if (expr.type !== "operator")
1018
- continue;
1019
- if (!this.expressions.has(expr.id))
1020
- continue;
1021
- const children = this.getChildExpressions(expr.id);
1022
- if (children.length === 0) {
1023
- const grandparentId = expr.parentId;
1024
- this.collector?.removedExpression({
1025
- ...expr,
1026
- });
1027
- this.detachExpression(expr.id, expr);
1028
- this.dirtyExpressionIds.delete(expr.id);
1029
- if (grandparentId !== null) {
1030
- this.markExpressionDirty(grandparentId);
1031
- }
1032
- changed = true;
1033
- }
1034
- else if (children.length === 1 && expr.operator !== "not") {
1035
- this.promoteChild(expr.id, expr, children[0]);
1036
- changed = true;
1037
- }
1038
- }
1039
- // Pass 2: Collapse unjustified formulas (bottom-up).
1040
- for (const expr of this.toArray()) {
1041
- if (expr.type !== "formula")
1042
- continue;
1043
- if (!this.expressions.has(expr.id))
1044
- continue;
1045
- const children = this.getChildExpressions(expr.id);
1046
- if (children.length === 0) {
1047
- const grandparentId = expr.parentId;
1048
- this.collector?.removedExpression({
1049
- ...expr,
1050
- });
1051
- this.detachExpression(expr.id, expr);
1052
- this.dirtyExpressionIds.delete(expr.id);
1053
- if (grandparentId !== null) {
1054
- this.markExpressionDirty(grandparentId);
1055
- }
1056
- changed = true;
1057
- }
1058
- else if (children.length === 1 &&
1059
- !this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
1060
- this.promoteChild(expr.id, expr, children[0]);
1061
- changed = true;
1062
- }
1063
- }
1064
- // Pass 3: Insert formula buffers for operator-under-operator violations.
1065
- for (const expr of this.toArray()) {
1066
- if (expr.type !== "operator" || expr.operator === "not")
1067
- continue;
1068
- if (!this.expressions.has(expr.id))
1069
- continue;
1070
- if (expr.parentId === null)
1071
- continue;
1072
- const parent = this.expressions.get(expr.parentId);
1073
- if (!parent || parent.type !== "operator")
1074
- continue;
1075
- // Non-not operator is direct child of operator — insert formula buffer.
1076
- const formulaPosition = expr.position;
1077
- const formulaParentId = expr.parentId;
1078
- const formulaId = this.registerFormulaBuffer(expr, formulaParentId, formulaPosition);
1079
- // Reparent the operator under the formula. This removes the
1080
- // operator's old position from the parent's position set, but
1081
- // the formula now occupies that slot, so re-add it. Also mark
1082
- // the formula dirty since it now has a child and its
1083
- // descendant/combined checksums need recomputation.
1084
- this.reparent(expr.id, formulaId, 0);
1085
- getOrCreate(this.childPositionsByParentId, formulaParentId, () => new Set()).add(formulaPosition);
1086
- this.markExpressionDirty(formulaId);
1087
- changed = true;
1088
- }
1089
- // Pass 4: Collapse double negation — NOT(NOT(x)) → x.
1090
- for (const expr of this.toArray()) {
1091
- if (expr.type !== "operator" || expr.operator !== "not")
1092
- continue;
1093
- if (!this.expressions.has(expr.id))
1094
- continue;
1095
- const children = this.getChildExpressions(expr.id);
1096
- if (children.length !== 1)
1097
- continue;
1098
- const child = children[0];
1099
- // Direct: NOT → NOT → x
1100
- if (child.type === "operator" && child.operator === "not") {
1101
- const innerChildren = this.getChildExpressions(child.id);
1102
- if (innerChildren.length === 1) {
1103
- // Promote inner child into outer NOT's slot, remove both NOTs.
1104
- this.promoteChild(child.id, child, innerChildren[0]);
1105
- this.promoteChild(expr.id,
1106
- // Re-fetch since promoteChild mutated in place.
1107
- this.expressions.get(expr.id), innerChildren[0]);
1108
- changed = true;
1109
- }
1110
- }
1111
- // Buffered: NOT → formula → NOT → x
1112
- if (child.type === "formula" &&
1113
- this.expressions.has(child.id)) {
1114
- const formulaChildren = this.getChildExpressions(child.id);
1115
- if (formulaChildren.length === 1 &&
1116
- formulaChildren[0].type === "operator" &&
1117
- formulaChildren[0].operator === "not") {
1118
- const innerNot = formulaChildren[0];
1119
- const innerChildren = this.getChildExpressions(innerNot.id);
1120
- if (innerChildren.length === 1) {
1121
- // Remove inner NOT, promote its child into formula.
1122
- this.promoteChild(innerNot.id, innerNot, innerChildren[0]);
1123
- // Remove outer NOT, promote formula into its slot.
1124
- this.promoteChild(expr.id, this.expressions.get(expr.id), child);
1125
- changed = true;
1126
- }
1127
- }
1128
- }
1129
- }
1130
- // Pass 5: Absorb same-operator through formula — OP → formula → OP → [...] becomes OP → [...].
1131
- for (const expr of this.toArray()) {
1132
- if (expr.type !== "formula")
1133
- continue;
1134
- if (!this.expressions.has(expr.id))
1135
- continue;
1136
- if (expr.parentId === null)
1137
- continue;
1138
- const parent = this.expressions.get(expr.parentId);
1139
- if (!parent || parent.type !== "operator")
1140
- continue;
1141
- if (parent.operator !== "and" && parent.operator !== "or")
1142
- continue;
1143
- const formulaChildren = this.getChildExpressions(expr.id);
1144
- if (formulaChildren.length !== 1)
1145
- continue;
1146
- const inner = formulaChildren[0];
1147
- if (inner.type !== "operator" ||
1148
- inner.operator !== parent.operator)
1149
- continue;
1150
- // Same operator on both sides — absorb inner's children into parent.
1151
- if (this.absorbSameOperator(inner.id)) {
1152
- changed = true;
1153
- }
1154
- }
1155
- }
1156
- }
739
+ // D2 — the inline AN cascades (`collapseIfNeeded` /
740
+ // `absorbSameOperatorIfNeeded` / `absorbSameOperator`), the legacy
741
+ // `ExpressionManager.normalize()` 5-pass sweep, the
742
+ // `promoteChild` helper, and the private
743
+ // `hasBinaryOperatorInBoundedSubtree` were all deleted here. The
744
+ // first three were gated on the legacy `collapseEmptyFormula` /
745
+ // `absorbSameOperator` flags and ran inline from
746
+ // `removeExpression` / `updateExpression`. The `normalize()` sweep
747
+ // was the underlying engine for `pe.normalizeExpressions()` /
748
+ // `engine.normalizeAllExpressions()`. All of these are subsumed by
749
+ // the four native AN passes in `src/lib/grammar/an-rules.ts`
750
+ // (AN-1..AN-4 assistive-mode post-mutation hook +
751
+ // `engine.normalize(tier?)`'s explicit pass). The AN-3 module
752
+ // uses its own bounded-subtree helper bound to
753
+ // `pe.getChildExpressions` (see `src/lib/grammar/bounded-subtree.ts`).
1157
754
  /** Returns `true` if any expression in the tree references the given variable ID. */
1158
755
  hasVariableReference(variableId) {
1159
756
  for (const expression of this.expressions.values()) {
@@ -1189,112 +786,41 @@ export class ExpressionManager {
1189
786
  }
1190
787
  }
1191
788
  /**
1192
- * Simulates the collapse chain that would result from removing an expression.
1193
- * Throws if any promotion would violate nesting or root-only rules.
789
+ * Pre-flight before {@link removeExpression} mutates state. As of v1.0
790
+ * the only structural failure mode for `removeAndPromote`'s 1-child
791
+ * branch is the root-only-operator promotion rule (S-5): an
792
+ * `implies`/`iff` child cannot be promoted into a non-root slot. The
793
+ * pre-v1.0 P-1 promote-on-remove check was deleted in D2 along with
794
+ * the rest of the `grammarConfig.enforceFormulaBetweenOperators`
795
+ * machinery; the legacy `collapseEmptyFormula` cascade simulation
796
+ * (`simulateCollapseChain` / `simulatePostPromotionCollapse`) was
797
+ * deleted in lockstep — AN-3 (post-mutation hook in assistive mode)
798
+ * handles every collapse case.
1194
799
  */
1195
800
  assertRemovalSafe(expressionId, deleteSubtree) {
801
+ if (deleteSubtree)
802
+ return;
1196
803
  const target = this.expressions.get(expressionId);
1197
804
  if (!target)
1198
805
  return;
1199
- if (!deleteSubtree) {
1200
- const children = this.getChildExpressions(expressionId);
1201
- // >1 children: removeAndPromote throws before any mutation, no nesting concern.
1202
- if (children.length === 1) {
1203
- this.assertPromotionSafe(children[0], target.parentId);
1204
- // Simulate post-promotion cascade (formula collapse after promotion).
1205
- if (resolveAutoNormalize(this.grammarConfig, "collapseEmptyFormula")) {
1206
- this.simulatePostPromotionCollapse(target.parentId, children[0]);
1207
- }
1208
- }
1209
- if (children.length === 0) {
1210
- this.simulateCollapseChain(target.parentId, expressionId);
1211
- }
1212
- return;
806
+ const children = this.getChildExpressions(expressionId);
807
+ if (children.length === 1) {
808
+ this.assertPromotionSafe(children[0], target.parentId);
1213
809
  }
1214
- // deleteSubtree: entire subtree removed, then collapse runs on parent.
1215
- this.simulateCollapseChain(target.parentId, expressionId);
1216
810
  }
1217
811
  /**
1218
812
  * Checks whether promoting `child` into a slot with the given `newParentId`
1219
- * would violate the nesting rule or root-only rule.
813
+ * would violate the root-only rule (S-5). The pre-v1.0 nesting check
814
+ * (P-1 / `enforceFormulaBetweenOperators`) was deleted in D2.
1220
815
  */
1221
816
  assertPromotionSafe(child, newParentId) {
1222
817
  if (child.type !== "operator")
1223
818
  return;
1224
- // Root-only check — always enforced
819
+ // Root-only check — always enforced (S-5)
1225
820
  if ((child.operator === "implies" || child.operator === "iff") &&
1226
821
  newParentId !== null) {
1227
822
  throw new Error(`Cannot remove expression — would promote a root-only operator ("${child.operator}") to a non-root position`);
1228
823
  }
1229
- // Nesting check — grammar-configurable
1230
- if (this.grammarConfig.enforceFormulaBetweenOperators) {
1231
- if (child.operator !== "not" && newParentId !== null) {
1232
- const newParent = this.expressions.get(newParentId);
1233
- if (newParent && newParent.type === "operator") {
1234
- throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
1235
- }
1236
- }
1237
- }
1238
- }
1239
- /**
1240
- * Walks the collapse chain starting from `operatorId` after `removedChildId`
1241
- * is removed. At each level: if 0 remaining children, operator/formula is deleted
1242
- * and chain continues up. If 1 remaining child, check promotion safety.
1243
- */
1244
- simulateCollapseChain(operatorId, removedChildId) {
1245
- if (!resolveAutoNormalize(this.grammarConfig, "collapseEmptyFormula"))
1246
- return;
1247
- if (operatorId === null)
1248
- return;
1249
- const operator = this.expressions.get(operatorId);
1250
- if (!operator)
1251
- return;
1252
- if (operator.type !== "operator" && operator.type !== "formula")
1253
- return;
1254
- const children = this.getChildExpressions(operatorId);
1255
- const remainingChildren = children.filter((c) => c.id !== removedChildId);
1256
- if (operator.type === "formula") {
1257
- if (remainingChildren.length === 0) {
1258
- this.simulateCollapseChain(operator.parentId, operatorId);
1259
- }
1260
- else if (remainingChildren.length === 1 &&
1261
- !this.hasBinaryOperatorInBoundedSubtree(remainingChildren[0].id)) {
1262
- // Formula would collapse — child promoted.
1263
- // Formula collapse promotion is always safe (child is variable, not, or formula).
1264
- this.simulateCollapseChain(operator.parentId, operatorId);
1265
- }
1266
- return;
1267
- }
1268
- // operator.type === "operator"
1269
- if (remainingChildren.length === 0) {
1270
- this.simulateCollapseChain(operator.parentId, operatorId);
1271
- }
1272
- else if (remainingChildren.length === 1) {
1273
- this.assertPromotionSafe(remainingChildren[0], operator.parentId);
1274
- // After promotion, simulate further collapse on grandparent.
1275
- this.simulatePostPromotionCollapse(operator.parentId, remainingChildren[0]);
1276
- }
1277
- }
1278
- /**
1279
- * After an operator promotion places `promotedChild` into `parentId`'s child set,
1280
- * check whether the parent (if a formula) would itself collapse. Formula collapse
1281
- * promotion is always safe (the child can't be a binary operator or root-only operator),
1282
- * but we need to continue the simulation chain.
1283
- */
1284
- simulatePostPromotionCollapse(parentId, promotedChild) {
1285
- if (parentId === null)
1286
- return;
1287
- const parent = this.expressions.get(parentId);
1288
- if (!parent)
1289
- return;
1290
- if (parent.type === "formula") {
1291
- if (!this.hasBinaryOperatorInBoundedSubtree(promotedChild.id)) {
1292
- // Formula would collapse. The promotedChild takes formula's slot.
1293
- // This is always safe. Continue simulation from formula's parent.
1294
- this.simulatePostPromotionCollapse(parent.parentId, promotedChild);
1295
- }
1296
- }
1297
- // Operator parents: child count unchanged, no further collapse.
1298
824
  }
1299
825
  assertChildLimit(operator, parentId) {
1300
826
  const childCount = this.childExpressionIdsByParentId.get(parentId)?.size ?? 0;
@@ -1424,56 +950,33 @@ export class ExpressionManager {
1424
950
  anchor.parentId !== null) {
1425
951
  throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
1426
952
  }
1427
- // 10a. Non-not operators cannot be direct children of operators.
1428
- // Track which children need formula buffers (Site 2) for post-reparent insertion.
1429
- let needsParentFormulaBuffer = false;
1430
- const childrenNeedingFormulaBuffer = [];
1431
- if (this.grammarConfig.enforceFormulaBetweenOperators) {
1432
- // Check 1 (Site 1): new expression as child of anchor's parent.
1433
- if (anchor.parentId !== null &&
1434
- expression.type === "operator" &&
1435
- expression.operator !== "not") {
1436
- const anchorParent = this.expressions.get(anchor.parentId);
1437
- if (anchorParent && anchorParent.type === "operator") {
1438
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1439
- needsParentFormulaBuffer = true;
1440
- }
1441
- else {
1442
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1443
- }
1444
- }
1445
- }
1446
- // Check 2 (Site 2): left/right nodes as children of the new expression.
1447
- if (expression.type === "operator") {
1448
- if (leftNode?.type === "operator" &&
1449
- leftNode.operator !== "not") {
1450
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1451
- childrenNeedingFormulaBuffer.push(leftNodeId);
1452
- }
1453
- else {
1454
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1455
- }
1456
- }
1457
- if (rightNode?.type === "operator" &&
1458
- rightNode.operator !== "not") {
1459
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1460
- childrenNeedingFormulaBuffer.push(rightNodeId);
1461
- }
1462
- else {
1463
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1464
- }
1465
- }
1466
- }
1467
- }
953
+ // D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
954
+ // (gated on `grammarConfig.enforceFormulaBetweenOperators` +
955
+ // `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
956
+ // sites (1) new expression as child of anchor's parent;
957
+ // (2) left node as child of new expression; (3) right node as
958
+ // child of new expression were deleted. AN-1 (post-mutation
959
+ // hook in assistive mode) inserts the buffer when any of these
960
+ // sites produces a non-not operator under operator;
961
+ // permissive mode leaves the un-buffered state and
962
+ // `validate('presentable')` flags it.
1468
963
  const anchorParentId = anchor.parentId;
1469
964
  const anchorPosition = anchor.position;
1470
- // Compute child positions (midpoint-spaced for future bisection),
1471
- // matching the pattern used by wrapExpression.
965
+ // Compute child positions. For `implies`/`iff` the S-8 invariant
966
+ // pins the two children to exact positions [0, 1] — bisection
967
+ // headroom is irrelevant because S-8 also fixes arity at 2. For
968
+ // `and`/`or` (and `formula`, though it's never produced here), use
969
+ // midpoint-spaced positions so future inserts can bisect. Mirrors
970
+ // the wrapExpression branch landed in c303aa4.
971
+ const isBinaryOp = expression.type === "operator" &&
972
+ (expression.operator === "implies" || expression.operator === "iff");
1472
973
  let leftPosition;
1473
974
  let rightPosition;
1474
975
  if (leftNodeId !== undefined && rightNodeId !== undefined) {
1475
- leftPosition = this.positionConfig.initial;
1476
- rightPosition = midpoint(this.positionConfig.initial, this.positionConfig.max);
976
+ leftPosition = isBinaryOp ? 0 : this.positionConfig.initial;
977
+ rightPosition = isBinaryOp
978
+ ? 1
979
+ : midpoint(this.positionConfig.initial, this.positionConfig.max);
1477
980
  }
1478
981
  else if (leftNodeId !== undefined) {
1479
982
  leftPosition = this.positionConfig.initial;
@@ -1490,39 +993,18 @@ export class ExpressionManager {
1490
993
  if (leftNodeId !== undefined) {
1491
994
  this.reparent(leftNodeId, expression.id, leftPosition);
1492
995
  }
1493
- // Determine the slot for the new expression. If a parent formula buffer
1494
- // is needed, the formula takes the anchor slot and the expression goes under it.
1495
- let finalParentId = anchorParentId;
1496
- let finalPosition = anchorPosition;
1497
- if (needsParentFormulaBuffer) {
1498
- const formulaId = this.registerFormulaBuffer(expression, anchorParentId, anchorPosition);
1499
- finalParentId = formulaId;
1500
- finalPosition = 0;
1501
- }
1502
- // Store the new expression in its slot.
996
+ // Store the new expression in the anchor's slot.
1503
997
  const stored = this.attachChecksum({
1504
998
  ...expression,
1505
- parentId: finalParentId,
1506
- position: finalPosition,
999
+ parentId: anchorParentId,
1000
+ position: anchorPosition,
1507
1001
  });
1508
1002
  this.expressions.set(expression.id, stored);
1509
1003
  this.collector?.addedExpression({
1510
1004
  ...stored,
1511
1005
  });
1512
- getOrCreate(this.childExpressionIdsByParentId, finalParentId, () => new Set()).add(expression.id);
1513
- getOrCreate(this.childPositionsByParentId, finalParentId, () => new Set()).add(finalPosition);
1514
- // Site 2: auto-insert formula buffers between the new expression and
1515
- // any offending operator children.
1516
- for (const childId of childrenNeedingFormulaBuffer) {
1517
- const child = this.expressions.get(childId);
1518
- const childPosition = child.position;
1519
- // Reparent the child under the formula first. This detaches the child
1520
- // from expression.id's tracking (removing its position from the set).
1521
- // registerFormulaBuffer then occupies the freed position.
1522
- const formulaId = this.generateId();
1523
- this.reparent(childId, formulaId, 0);
1524
- this.registerFormulaBuffer(expression, expression.id, childPosition, formulaId);
1525
- }
1006
+ getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(expression.id);
1007
+ getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
1526
1008
  // Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
1527
1009
  // Note: reparent() already marks children dirty, so this propagates from the new expression up.
1528
1010
  this.markExpressionDirty(expression.id);
@@ -1597,56 +1079,39 @@ export class ExpressionManager {
1597
1079
  (newSibling.operator === "implies" || newSibling.operator === "iff")) {
1598
1080
  throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
1599
1081
  }
1600
- // 10a. Non-not operators cannot be direct children of operators.
1601
- // Track which sites need formula buffers for post-mutation insertion.
1602
- let needsParentFormulaBuffer = false;
1603
- let existingNodeNeedsFormulaBuffer = false;
1604
- let siblingNeedsFormulaBuffer = false;
1605
- if (this.grammarConfig.enforceFormulaBetweenOperators) {
1606
- // Check 1 (Site 1): new operator as child of existing node's parent.
1607
- // Note: step 7 already rejects `not`, so operator.operator is always non-not here.
1608
- if (existingNode.parentId !== null) {
1609
- const existingParent = this.expressions.get(existingNode.parentId);
1610
- if (existingParent && existingParent.type === "operator") {
1611
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1612
- needsParentFormulaBuffer = true;
1613
- }
1614
- else {
1615
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1616
- }
1617
- }
1618
- }
1619
- // Check 2 (Site 2): existing node as child of new operator.
1620
- if (existingNode.type === "operator" &&
1621
- existingNode.operator !== "not") {
1622
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1623
- existingNodeNeedsFormulaBuffer = true;
1624
- }
1625
- else {
1626
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1627
- }
1628
- }
1629
- // Check 3 (Site 3): new sibling as child of new operator.
1630
- if (newSibling.type === "operator" &&
1631
- newSibling.operator !== "not") {
1632
- if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
1633
- siblingNeedsFormulaBuffer = true;
1634
- }
1635
- else {
1636
- throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
1637
- }
1638
- }
1639
- }
1082
+ // D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
1083
+ // (gated on `grammarConfig.enforceFormulaBetweenOperators` +
1084
+ // `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
1085
+ // sites (1) new operator as child of existing node's parent;
1086
+ // (2) existing node as child of new operator;
1087
+ // (3) new sibling as child of new operator — were deleted.
1088
+ // AN-1 (post-mutation hook in assistive mode) inserts the
1089
+ // buffer when any of these sites produces a non-not operator
1090
+ // under operator; permissive mode leaves the un-buffered state
1091
+ // and `validate('presentable')` flags it.
1640
1092
  // Save the existing node's slot (the operator will inherit it).
1641
1093
  const anchorParentId = existingNode.parentId;
1642
1094
  const anchorPosition = existingNode.position;
1643
- // Determine child positions (midpoint-spaced for future bisection).
1095
+ // Determine child positions. For `implies`/`iff` the S-8 invariant
1096
+ // pins the two children to exact positions [0, 1] — bisection
1097
+ // headroom is irrelevant because S-8 also fixes arity at 2. For
1098
+ // `and`/`or` (and `formula`, though it's never produced here), use
1099
+ // midpoint-spaced positions so future inserts can bisect.
1100
+ const isBinaryOp = operator.operator === "implies" || operator.operator === "iff";
1644
1101
  const existingPosition = leftNodeId !== undefined
1645
- ? this.positionConfig.initial
1646
- : midpoint(this.positionConfig.initial, this.positionConfig.max);
1102
+ ? isBinaryOp
1103
+ ? 0
1104
+ : this.positionConfig.initial
1105
+ : isBinaryOp
1106
+ ? 1
1107
+ : midpoint(this.positionConfig.initial, this.positionConfig.max);
1647
1108
  const siblingPosition = leftNodeId !== undefined
1648
- ? midpoint(this.positionConfig.initial, this.positionConfig.max)
1649
- : this.positionConfig.initial;
1109
+ ? isBinaryOp
1110
+ ? 1
1111
+ : midpoint(this.positionConfig.initial, this.positionConfig.max)
1112
+ : isBinaryOp
1113
+ ? 0
1114
+ : this.positionConfig.initial;
1650
1115
  // Reparent existing node under operator.
1651
1116
  this.reparent(existingNodeId, operator.id, existingPosition);
1652
1117
  // Store new sibling under operator.
@@ -1661,47 +1126,18 @@ export class ExpressionManager {
1661
1126
  });
1662
1127
  getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
1663
1128
  getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(siblingPosition);
1664
- // Determine the operator's slot. If a parent formula buffer is needed,
1665
- // the formula takes the anchor slot and the operator goes under it.
1666
- let operatorParentId = anchorParentId;
1667
- let operatorPosition = anchorPosition;
1668
- if (needsParentFormulaBuffer) {
1669
- const formulaId = this.registerFormulaBuffer(operator, anchorParentId, anchorPosition);
1670
- operatorParentId = formulaId;
1671
- operatorPosition = 0;
1672
- }
1673
- // Store operator in its slot.
1129
+ // Store operator in the anchor slot.
1674
1130
  const storedOperator = this.attachChecksum({
1675
1131
  ...operator,
1676
- parentId: operatorParentId,
1677
- position: operatorPosition,
1132
+ parentId: anchorParentId,
1133
+ position: anchorPosition,
1678
1134
  });
1679
1135
  this.expressions.set(operator.id, storedOperator);
1680
1136
  this.collector?.addedExpression({
1681
1137
  ...storedOperator,
1682
1138
  });
1683
- getOrCreate(this.childExpressionIdsByParentId, operatorParentId, () => new Set()).add(operator.id);
1684
- getOrCreate(this.childPositionsByParentId, operatorParentId, () => new Set()).add(operatorPosition);
1685
- // Site 2: auto-insert formula buffer between operator and existing node.
1686
- if (existingNodeNeedsFormulaBuffer) {
1687
- const existingChild = this.expressions.get(existingNodeId);
1688
- const childPosition = existingChild.position;
1689
- const formulaId = this.generateId();
1690
- // Reparent existing node under formula first (frees position in operator's tracking).
1691
- // registerFormulaBuffer then occupies the freed position.
1692
- this.reparent(existingNodeId, formulaId, 0);
1693
- this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
1694
- }
1695
- // Site 3: auto-insert formula buffer between operator and new sibling.
1696
- if (siblingNeedsFormulaBuffer) {
1697
- const siblingChild = this.expressions.get(newSibling.id);
1698
- const childPosition = siblingChild.position;
1699
- const formulaId = this.generateId();
1700
- // Reparent sibling under formula first (frees position in operator's tracking).
1701
- // registerFormulaBuffer then occupies the freed position.
1702
- this.reparent(newSibling.id, formulaId, 0);
1703
- this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
1704
- }
1139
+ getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(operator.id);
1140
+ getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
1705
1141
  // Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
1706
1142
  // reparent() already marks the existing node dirty; mark the operator and sibling as well.
1707
1143
  this.markExpressionDirty(newSibling.id);
@@ -1717,6 +1153,80 @@ export class ExpressionManager {
1717
1153
  }
1718
1154
  this.reparent(expressionId, newParentId, newPosition);
1719
1155
  }
1156
+ /**
1157
+ * Inserts a new `formula` node between an existing expression and
1158
+ * its current parent atomically. The formula takes the child's
1159
+ * original slot (parentId + position); the child becomes the
1160
+ * formula's sole child at position 0.
1161
+ *
1162
+ * Used by the native AN-1 (formula-buffer insertion) pass in
1163
+ * `src/lib/grammar/an-rules.ts` per spec §5.1. Composing this from
1164
+ * `addExpression` + `reparentExpression` is not possible without
1165
+ * trip-wires: `addExpression(formula, parent, childPosition)` would
1166
+ * throw S-9 (child still occupies that slot), and `assertChildLimit`
1167
+ * would reject the extra child under unary `not` parents and binary
1168
+ * `implies`/`iff` parents even transiently. This primitive sidesteps
1169
+ * both: the net child count of the parent is unchanged by the wrap
1170
+ * (the formula displaces the child), so the limit isn't actually
1171
+ * violated, just transiently if expressed via two atomic mutations.
1172
+ *
1173
+ * Generates the formula's id via the caller-supplied `formulaId`
1174
+ * parameter so the caller (PE) can plug in the engine's
1175
+ * `idGenerator` rather than this manager minting one internally —
1176
+ * keeps id provenance explicit at the PE boundary.
1177
+ *
1178
+ * The new formula inherits the source child's `argumentId`,
1179
+ * `argumentVersion`, and `premiseId` automatically.
1180
+ *
1181
+ * @throws If `childId` does not exist.
1182
+ * @throws If `childId` is at the root (`parentId === null`) — there
1183
+ * is no operator parent to insert a buffer beneath.
1184
+ */
1185
+ wrapInFormula(childId, formulaId) {
1186
+ const child = this.expressions.get(childId);
1187
+ if (!child) {
1188
+ throw new Error(`Expression "${childId}" does not exist.`);
1189
+ }
1190
+ if (child.parentId === null) {
1191
+ throw new Error(`Cannot wrap root expression "${childId}" in a formula — no parent operator above it.`);
1192
+ }
1193
+ // S-10 entity-ID uniqueness: refuse to mint a formula at an id
1194
+ // that already exists in this premise. Without this check
1195
+ // `registerFormulaBuffer` would silently overwrite the prior
1196
+ // expression via `this.expressions.set`. Today's AN-1 caller
1197
+ // mints via `engine.idGenerator` (crypto UUID v4) so collision
1198
+ // is astronomically unlikely, but `wrapInFormula` is a public
1199
+ // bundled-composite primitive — the API surface promises S-10
1200
+ // enforcement regardless of immediate caller.
1201
+ if (this.expressions.has(formulaId)) {
1202
+ throw new Error(`S-10: formulaId "${formulaId}" already exists in this premise.`);
1203
+ }
1204
+ const childParentId = child.parentId;
1205
+ const childPosition = child.position;
1206
+ // Register the formula at the child's old slot. Bypasses the
1207
+ // S-9 check via direct map insertion — at this transient point
1208
+ // the child still nominally occupies childPosition (its
1209
+ // parentId/position fields are unchanged), but the position
1210
+ // *set* under childParentId already contains childPosition (it
1211
+ // was added when the child was inserted), and Set.add is
1212
+ // idempotent, so re-adding it for the formula is safe. The
1213
+ // very next reparent call moves the child to (formulaId, 0),
1214
+ // freeing childPosition from the child's tracking and leaving
1215
+ // it owned by the formula alone.
1216
+ // `child` is already typed `TExpr` from `this.expressions.get`,
1217
+ // so no cast is needed at this call site.
1218
+ this.registerFormulaBuffer(child, childParentId, childPosition, formulaId);
1219
+ // Move the child under the new formula at position 0. The
1220
+ // child's old slot under childParentId is freed by `reparent`,
1221
+ // then the formula's `add(childPosition)` we did above keeps
1222
+ // childPosition owned by the formula. Result: a single formula
1223
+ // sits where the child was, the child is the formula's only
1224
+ // descendant at position 0.
1225
+ this.reparent(childId, formulaId, 0);
1226
+ // Mark the formula dirty so its descendant/combined checksums
1227
+ // recompute (it now has a child).
1228
+ this.markExpressionDirty(formulaId);
1229
+ }
1720
1230
  /**
1721
1231
  * Deletes a single expression that has no children.
1722
1232
  * Does NOT trigger operator collapse. Caller must ensure children
@@ -1773,8 +1283,9 @@ export class ExpressionManager {
1773
1283
  ...updated,
1774
1284
  });
1775
1285
  this.markExpressionDirty(expressionId);
1776
- // After the operator change, absorb same-operator children through a formula.
1777
- this.absorbSameOperatorIfNeeded(expressionId);
1286
+ // D2: AN-4 (post-mutation hook in assistive mode) handles
1287
+ // same-operator absorption through a formula buffer if this
1288
+ // operator change produced one.
1778
1289
  return this.expressions.get(expressionId) ?? updated;
1779
1290
  }
1780
1291
  /**
@@ -1878,21 +1389,13 @@ export class ExpressionManager {
1878
1389
  entityId: id,
1879
1390
  });
1880
1391
  }
1881
- // 3g. Formula-between-operators
1882
- if (this.grammarConfig.enforceFormulaBetweenOperators &&
1883
- expr.parentId !== null &&
1884
- expr.type === "operator" &&
1885
- expr.operator !== "not") {
1886
- const parent = this.expressions.get(expr.parentId);
1887
- if (parent && parent.type === "operator") {
1888
- violations.push({
1889
- code: EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED,
1890
- message: `Non-not operator "${expr.operator}" expression "${id}" is a direct child of operator "${expr.parentId}".`,
1891
- entityType: "expression",
1892
- entityId: id,
1893
- });
1894
- }
1895
- }
1392
+ // D2: the pre-v1.0 3g `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
1393
+ // legacy-validate() check (gated on
1394
+ // `grammarConfig.enforceFormulaBetweenOperators`) is gone.
1395
+ // P-1 is now surfaced via the grammar-tier validators —
1396
+ // call `engine.validate('presentable')` and look for the
1397
+ // `P-1` code. The `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
1398
+ // engine-error constant was deleted in lockstep.
1896
1399
  // Collect positions for uniqueness check
1897
1400
  const parentKey = expr.parentId;
1898
1401
  let parentPositions = positionsByParent.get(parentKey);
@@ -1973,7 +1476,7 @@ export class ExpressionManager {
1973
1476
  };
1974
1477
  }
1975
1478
  /** Creates a new ExpressionManager from a previously captured snapshot. */
1976
- static fromSnapshot(snapshot, grammarConfig, generateId) {
1479
+ static fromSnapshot(snapshot, generateId) {
1977
1480
  // Normalize checksumConfig in case the snapshot went through a JSON
1978
1481
  // round-trip that converted Sets to arrays or empty objects.
1979
1482
  const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
@@ -1983,10 +1486,8 @@ export class ExpressionManager {
1983
1486
  checksumConfig: normalizedChecksumConfig,
1984
1487
  }
1985
1488
  : undefined;
1986
- // During loading: use explicit grammarConfig, falling back to snapshot's config
1987
1489
  const loadingConfig = {
1988
1490
  ...normalizedConfig,
1989
- grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
1990
1491
  generateId: generateId ?? normalizedConfig?.generateId,
1991
1492
  };
1992
1493
  const em = new ExpressionManager(loadingConfig);