@proposit/proposit-core 0.12.1 → 1.0.0

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 (175) 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/cli/schemata.d.ts.map +1 -1
  18. package/dist/cli/schemata.js +4 -3
  19. package/dist/cli/schemata.js.map +1 -1
  20. package/dist/extensions/basics/schemata.d.ts +56 -2
  21. package/dist/extensions/basics/schemata.d.ts.map +1 -1
  22. package/dist/extensions/basics/schemata.js +54 -23
  23. package/dist/extensions/basics/schemata.js.map +1 -1
  24. package/dist/lib/core/argument-engine.d.ts +275 -10
  25. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  26. package/dist/lib/core/argument-engine.js +442 -82
  27. package/dist/lib/core/argument-engine.js.map +1 -1
  28. package/dist/lib/core/argument-library.d.ts +5 -1
  29. package/dist/lib/core/argument-library.d.ts.map +1 -1
  30. package/dist/lib/core/argument-library.js +7 -3
  31. package/dist/lib/core/argument-library.js.map +1 -1
  32. package/dist/lib/core/claim-axiom-library.d.ts +3 -3
  33. package/dist/lib/core/claim-axiom-library.d.ts.map +1 -1
  34. package/dist/lib/core/claim-axiom-library.js +2 -2
  35. package/dist/lib/core/claim-axiom-library.js.map +1 -1
  36. package/dist/lib/core/claim-citation-library.d.ts +3 -3
  37. package/dist/lib/core/claim-citation-library.d.ts.map +1 -1
  38. package/dist/lib/core/claim-citation-library.js +2 -2
  39. package/dist/lib/core/claim-citation-library.js.map +1 -1
  40. package/dist/lib/core/expression-manager.d.ts +68 -73
  41. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  42. package/dist/lib/core/expression-manager.js +242 -762
  43. package/dist/lib/core/expression-manager.js.map +1 -1
  44. package/dist/lib/core/fork.d.ts +5 -1
  45. package/dist/lib/core/fork.d.ts.map +1 -1
  46. package/dist/lib/core/fork.js +16 -6
  47. package/dist/lib/core/fork.js.map +1 -1
  48. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +68 -7
  49. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  50. package/dist/lib/core/interfaces/library.interfaces.d.ts +9 -6
  51. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  52. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +50 -47
  53. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  54. package/dist/lib/core/premise-engine.d.ts +80 -11
  55. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  56. package/dist/lib/core/premise-engine.js +232 -80
  57. package/dist/lib/core/premise-engine.js.map +1 -1
  58. package/dist/lib/core/proposit-core.d.ts +5 -5
  59. package/dist/lib/core/proposit-core.d.ts.map +1 -1
  60. package/dist/lib/core/proposit-core.js +13 -3
  61. package/dist/lib/core/proposit-core.js.map +1 -1
  62. package/dist/lib/grammar/an-rules.d.ts +158 -0
  63. package/dist/lib/grammar/an-rules.d.ts.map +1 -0
  64. package/dist/lib/grammar/an-rules.js +778 -0
  65. package/dist/lib/grammar/an-rules.js.map +1 -0
  66. package/dist/lib/grammar/auto-normalize.d.ts +14 -0
  67. package/dist/lib/grammar/auto-normalize.d.ts.map +1 -0
  68. package/dist/lib/grammar/auto-normalize.js +35 -0
  69. package/dist/lib/grammar/auto-normalize.js.map +1 -0
  70. package/dist/lib/grammar/bounded-subtree.d.ts +30 -0
  71. package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -0
  72. package/dist/lib/grammar/bounded-subtree.js +74 -0
  73. package/dist/lib/grammar/bounded-subtree.js.map +1 -0
  74. package/dist/lib/grammar/naked-q.d.ts +20 -0
  75. package/dist/lib/grammar/naked-q.d.ts.map +1 -0
  76. package/dist/lib/grammar/naked-q.js +58 -0
  77. package/dist/lib/grammar/naked-q.js.map +1 -0
  78. package/dist/lib/grammar/normalize.d.ts +12 -0
  79. package/dist/lib/grammar/normalize.d.ts.map +1 -0
  80. package/dist/lib/grammar/normalize.js +45 -0
  81. package/dist/lib/grammar/normalize.js.map +1 -0
  82. package/dist/lib/grammar/populate-from.d.ts +25 -0
  83. package/dist/lib/grammar/populate-from.d.ts.map +1 -0
  84. package/dist/lib/grammar/populate-from.js +252 -0
  85. package/dist/lib/grammar/populate-from.js.map +1 -0
  86. package/dist/lib/grammar/repair.d.ts +65 -0
  87. package/dist/lib/grammar/repair.d.ts.map +1 -0
  88. package/dist/lib/grammar/repair.js +251 -0
  89. package/dist/lib/grammar/repair.js.map +1 -0
  90. package/dist/lib/grammar/types.d.ts +17 -0
  91. package/dist/lib/grammar/types.d.ts.map +1 -0
  92. package/dist/lib/grammar/types.js +82 -0
  93. package/dist/lib/grammar/types.js.map +1 -0
  94. package/dist/lib/grammar/validate.d.ts +4 -0
  95. package/dist/lib/grammar/validate.d.ts.map +1 -0
  96. package/dist/lib/grammar/validate.js +28 -0
  97. package/dist/lib/grammar/validate.js.map +1 -0
  98. package/dist/lib/grammar/validators/context.d.ts +11 -0
  99. package/dist/lib/grammar/validators/context.d.ts.map +1 -0
  100. package/dist/lib/grammar/validators/context.js +5 -0
  101. package/dist/lib/grammar/validators/context.js.map +1 -0
  102. package/dist/lib/grammar/validators/derivable.d.ts +63 -0
  103. package/dist/lib/grammar/validators/derivable.d.ts.map +1 -0
  104. package/dist/lib/grammar/validators/derivable.js +502 -0
  105. package/dist/lib/grammar/validators/derivable.js.map +1 -0
  106. package/dist/lib/grammar/validators/evaluable.d.ts +48 -0
  107. package/dist/lib/grammar/validators/evaluable.d.ts.map +1 -0
  108. package/dist/lib/grammar/validators/evaluable.js +226 -0
  109. package/dist/lib/grammar/validators/evaluable.js.map +1 -0
  110. package/dist/lib/grammar/validators/presentable.d.ts +45 -0
  111. package/dist/lib/grammar/validators/presentable.d.ts.map +1 -0
  112. package/dist/lib/grammar/validators/presentable.js +231 -0
  113. package/dist/lib/grammar/validators/presentable.js.map +1 -0
  114. package/dist/lib/grammar/validators/structural.d.ts +103 -0
  115. package/dist/lib/grammar/validators/structural.d.ts.map +1 -0
  116. package/dist/lib/grammar/validators/structural.js +602 -0
  117. package/dist/lib/grammar/validators/structural.js.map +1 -0
  118. package/dist/lib/index.d.ts +4 -3
  119. package/dist/lib/index.d.ts.map +1 -1
  120. package/dist/lib/index.js +2 -2
  121. package/dist/lib/index.js.map +1 -1
  122. package/dist/lib/parsing/argument-parser.d.ts +8 -5
  123. package/dist/lib/parsing/argument-parser.d.ts.map +1 -1
  124. package/dist/lib/parsing/argument-parser.js +204 -42
  125. package/dist/lib/parsing/argument-parser.js.map +1 -1
  126. package/dist/lib/parsing/clamp-max-lengths.d.ts.map +1 -1
  127. package/dist/lib/parsing/clamp-max-lengths.js +10 -4
  128. package/dist/lib/parsing/clamp-max-lengths.js.map +1 -1
  129. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
  130. package/dist/lib/parsing/prompt-builder.js +13 -15
  131. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  132. package/dist/lib/parsing/schemata.d.ts +0 -3
  133. package/dist/lib/parsing/schemata.d.ts.map +1 -1
  134. package/dist/lib/parsing/schemata.js +25 -13
  135. package/dist/lib/parsing/schemata.js.map +1 -1
  136. package/dist/lib/parsing/types.d.ts +1 -1
  137. package/dist/lib/parsing/types.d.ts.map +1 -1
  138. package/dist/lib/schemata/claim.d.ts +8 -0
  139. package/dist/lib/schemata/claim.d.ts.map +1 -1
  140. package/dist/lib/schemata/claim.js +17 -7
  141. package/dist/lib/schemata/claim.js.map +1 -1
  142. package/dist/lib/schemata/import.d.ts.map +1 -1
  143. package/dist/lib/schemata/import.js +2 -5
  144. package/dist/lib/schemata/import.js.map +1 -1
  145. package/dist/lib/schemata/index.d.ts +0 -2
  146. package/dist/lib/schemata/index.d.ts.map +1 -1
  147. package/dist/lib/schemata/index.js +0 -2
  148. package/dist/lib/schemata/index.js.map +1 -1
  149. package/dist/lib/types/evaluation.d.ts +1 -1
  150. package/dist/lib/types/evaluation.d.ts.map +1 -1
  151. package/dist/lib/types/fork.d.ts +12 -3
  152. package/dist/lib/types/fork.d.ts.map +1 -1
  153. package/dist/lib/types/validation.d.ts +0 -6
  154. package/dist/lib/types/validation.d.ts.map +1 -1
  155. package/dist/lib/types/validation.js +23 -6
  156. package/dist/lib/types/validation.js.map +1 -1
  157. package/dist/lib/utils/lookup.d.ts +2 -2
  158. package/dist/lib/utils/lookup.js +2 -2
  159. package/package.json +1 -1
  160. package/dist/lib/core/managed-derivation-premise-engine.d.ts +0 -172
  161. package/dist/lib/core/managed-derivation-premise-engine.d.ts.map +0 -1
  162. package/dist/lib/core/managed-derivation-premise-engine.js +0 -550
  163. package/dist/lib/core/managed-derivation-premise-engine.js.map +0 -1
  164. package/dist/lib/schemata/claim-axiom.d.ts +0 -11
  165. package/dist/lib/schemata/claim-axiom.d.ts.map +0 -1
  166. package/dist/lib/schemata/claim-axiom.js +0 -9
  167. package/dist/lib/schemata/claim-axiom.js.map +0 -1
  168. package/dist/lib/schemata/claim-citation.d.ts +0 -11
  169. package/dist/lib/schemata/claim-citation.d.ts.map +0 -1
  170. package/dist/lib/schemata/claim-citation.js +0 -9
  171. package/dist/lib/schemata/claim-citation.js.map +0 -1
  172. package/dist/lib/types/grammar.d.ts +0 -83
  173. package/dist/lib/types/grammar.d.ts.map +0 -1
  174. package/dist/lib/types/grammar.js +0 -24
  175. 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,47 +950,16 @@ 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
965
  // Compute child positions (midpoint-spaced for future bisection),
@@ -1490,39 +985,18 @@ export class ExpressionManager {
1490
985
  if (leftNodeId !== undefined) {
1491
986
  this.reparent(leftNodeId, expression.id, leftPosition);
1492
987
  }
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.
988
+ // Store the new expression in the anchor's slot.
1503
989
  const stored = this.attachChecksum({
1504
990
  ...expression,
1505
- parentId: finalParentId,
1506
- position: finalPosition,
991
+ parentId: anchorParentId,
992
+ position: anchorPosition,
1507
993
  });
1508
994
  this.expressions.set(expression.id, stored);
1509
995
  this.collector?.addedExpression({
1510
996
  ...stored,
1511
997
  });
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
- }
998
+ getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(expression.id);
999
+ getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
1526
1000
  // Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
1527
1001
  // Note: reparent() already marks children dirty, so this propagates from the new expression up.
1528
1002
  this.markExpressionDirty(expression.id);
@@ -1597,46 +1071,16 @@ export class ExpressionManager {
1597
1071
  (newSibling.operator === "implies" || newSibling.operator === "iff")) {
1598
1072
  throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
1599
1073
  }
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
- }
1074
+ // D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
1075
+ // (gated on `grammarConfig.enforceFormulaBetweenOperators` +
1076
+ // `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
1077
+ // sites (1) new operator as child of existing node's parent;
1078
+ // (2) existing node as child of new operator;
1079
+ // (3) new sibling as child of new operator — were deleted.
1080
+ // AN-1 (post-mutation hook in assistive mode) inserts the
1081
+ // buffer when any of these sites produces a non-not operator
1082
+ // under operator; permissive mode leaves the un-buffered state
1083
+ // and `validate('presentable')` flags it.
1640
1084
  // Save the existing node's slot (the operator will inherit it).
1641
1085
  const anchorParentId = existingNode.parentId;
1642
1086
  const anchorPosition = existingNode.position;
@@ -1661,47 +1105,18 @@ export class ExpressionManager {
1661
1105
  });
1662
1106
  getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
1663
1107
  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.
1108
+ // Store operator in the anchor slot.
1674
1109
  const storedOperator = this.attachChecksum({
1675
1110
  ...operator,
1676
- parentId: operatorParentId,
1677
- position: operatorPosition,
1111
+ parentId: anchorParentId,
1112
+ position: anchorPosition,
1678
1113
  });
1679
1114
  this.expressions.set(operator.id, storedOperator);
1680
1115
  this.collector?.addedExpression({
1681
1116
  ...storedOperator,
1682
1117
  });
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
- }
1118
+ getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(operator.id);
1119
+ getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
1705
1120
  // Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
1706
1121
  // reparent() already marks the existing node dirty; mark the operator and sibling as well.
1707
1122
  this.markExpressionDirty(newSibling.id);
@@ -1717,6 +1132,80 @@ export class ExpressionManager {
1717
1132
  }
1718
1133
  this.reparent(expressionId, newParentId, newPosition);
1719
1134
  }
1135
+ /**
1136
+ * Inserts a new `formula` node between an existing expression and
1137
+ * its current parent atomically. The formula takes the child's
1138
+ * original slot (parentId + position); the child becomes the
1139
+ * formula's sole child at position 0.
1140
+ *
1141
+ * Used by the native AN-1 (formula-buffer insertion) pass in
1142
+ * `src/lib/grammar/an-rules.ts` per spec §5.1. Composing this from
1143
+ * `addExpression` + `reparentExpression` is not possible without
1144
+ * trip-wires: `addExpression(formula, parent, childPosition)` would
1145
+ * throw S-9 (child still occupies that slot), and `assertChildLimit`
1146
+ * would reject the extra child under unary `not` parents and binary
1147
+ * `implies`/`iff` parents even transiently. This primitive sidesteps
1148
+ * both: the net child count of the parent is unchanged by the wrap
1149
+ * (the formula displaces the child), so the limit isn't actually
1150
+ * violated, just transiently if expressed via two atomic mutations.
1151
+ *
1152
+ * Generates the formula's id via the caller-supplied `formulaId`
1153
+ * parameter so the caller (PE) can plug in the engine's
1154
+ * `idGenerator` rather than this manager minting one internally —
1155
+ * keeps id provenance explicit at the PE boundary.
1156
+ *
1157
+ * The new formula inherits the source child's `argumentId`,
1158
+ * `argumentVersion`, and `premiseId` automatically.
1159
+ *
1160
+ * @throws If `childId` does not exist.
1161
+ * @throws If `childId` is at the root (`parentId === null`) — there
1162
+ * is no operator parent to insert a buffer beneath.
1163
+ */
1164
+ wrapInFormula(childId, formulaId) {
1165
+ const child = this.expressions.get(childId);
1166
+ if (!child) {
1167
+ throw new Error(`Expression "${childId}" does not exist.`);
1168
+ }
1169
+ if (child.parentId === null) {
1170
+ throw new Error(`Cannot wrap root expression "${childId}" in a formula — no parent operator above it.`);
1171
+ }
1172
+ // S-10 entity-ID uniqueness: refuse to mint a formula at an id
1173
+ // that already exists in this premise. Without this check
1174
+ // `registerFormulaBuffer` would silently overwrite the prior
1175
+ // expression via `this.expressions.set`. Today's AN-1 caller
1176
+ // mints via `engine.idGenerator` (crypto UUID v4) so collision
1177
+ // is astronomically unlikely, but `wrapInFormula` is a public
1178
+ // bundled-composite primitive — the API surface promises S-10
1179
+ // enforcement regardless of immediate caller.
1180
+ if (this.expressions.has(formulaId)) {
1181
+ throw new Error(`S-10: formulaId "${formulaId}" already exists in this premise.`);
1182
+ }
1183
+ const childParentId = child.parentId;
1184
+ const childPosition = child.position;
1185
+ // Register the formula at the child's old slot. Bypasses the
1186
+ // S-9 check via direct map insertion — at this transient point
1187
+ // the child still nominally occupies childPosition (its
1188
+ // parentId/position fields are unchanged), but the position
1189
+ // *set* under childParentId already contains childPosition (it
1190
+ // was added when the child was inserted), and Set.add is
1191
+ // idempotent, so re-adding it for the formula is safe. The
1192
+ // very next reparent call moves the child to (formulaId, 0),
1193
+ // freeing childPosition from the child's tracking and leaving
1194
+ // it owned by the formula alone.
1195
+ // `child` is already typed `TExpr` from `this.expressions.get`,
1196
+ // so no cast is needed at this call site.
1197
+ this.registerFormulaBuffer(child, childParentId, childPosition, formulaId);
1198
+ // Move the child under the new formula at position 0. The
1199
+ // child's old slot under childParentId is freed by `reparent`,
1200
+ // then the formula's `add(childPosition)` we did above keeps
1201
+ // childPosition owned by the formula. Result: a single formula
1202
+ // sits where the child was, the child is the formula's only
1203
+ // descendant at position 0.
1204
+ this.reparent(childId, formulaId, 0);
1205
+ // Mark the formula dirty so its descendant/combined checksums
1206
+ // recompute (it now has a child).
1207
+ this.markExpressionDirty(formulaId);
1208
+ }
1720
1209
  /**
1721
1210
  * Deletes a single expression that has no children.
1722
1211
  * Does NOT trigger operator collapse. Caller must ensure children
@@ -1773,8 +1262,9 @@ export class ExpressionManager {
1773
1262
  ...updated,
1774
1263
  });
1775
1264
  this.markExpressionDirty(expressionId);
1776
- // After the operator change, absorb same-operator children through a formula.
1777
- this.absorbSameOperatorIfNeeded(expressionId);
1265
+ // D2: AN-4 (post-mutation hook in assistive mode) handles
1266
+ // same-operator absorption through a formula buffer if this
1267
+ // operator change produced one.
1778
1268
  return this.expressions.get(expressionId) ?? updated;
1779
1269
  }
1780
1270
  /**
@@ -1878,21 +1368,13 @@ export class ExpressionManager {
1878
1368
  entityId: id,
1879
1369
  });
1880
1370
  }
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
- }
1371
+ // D2: 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 was deleted in lockstep.
1896
1378
  // Collect positions for uniqueness check
1897
1379
  const parentKey = expr.parentId;
1898
1380
  let parentPositions = positionsByParent.get(parentKey);
@@ -1973,7 +1455,7 @@ export class ExpressionManager {
1973
1455
  };
1974
1456
  }
1975
1457
  /** Creates a new ExpressionManager from a previously captured snapshot. */
1976
- static fromSnapshot(snapshot, grammarConfig, generateId) {
1458
+ static fromSnapshot(snapshot, generateId) {
1977
1459
  // Normalize checksumConfig in case the snapshot went through a JSON
1978
1460
  // round-trip that converted Sets to arrays or empty objects.
1979
1461
  const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
@@ -1983,10 +1465,8 @@ export class ExpressionManager {
1983
1465
  checksumConfig: normalizedChecksumConfig,
1984
1466
  }
1985
1467
  : undefined;
1986
- // During loading: use explicit grammarConfig, falling back to snapshot's config
1987
1468
  const loadingConfig = {
1988
1469
  ...normalizedConfig,
1989
- grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
1990
1470
  generateId: generateId ?? normalizedConfig?.generateId,
1991
1471
  };
1992
1472
  const em = new ExpressionManager(loadingConfig);